Compare commits

..

57 Commits

Author SHA1 Message Date
78addbd8e3 Incorporated issues 2024-03-28 07:00:37 +01:00
304d3aa2e0 Corrected copyright 2024-03-28 06:59:39 +01:00
f44291e278 Disabled option to do transaction from view 2024-03-28 06:58:59 +01:00
3be16781e7 Added copyright 2024-03-17 15:29:12 +01:00
4fffc1c696 Set pubDate to published time and date 2024-03-17 09:41:09 +01:00
ceab7281e9 Now everyone only sees their own rejected articles 2024-03-17 09:15:37 +01:00
450dd79e51 Added ability to view tags when rejecting and change tags when reworking articles 2024-03-17 08:46:49 +01:00
c45df4bf1a Implemented retry logic on all transactions 2024-03-15 18:37:24 +01:00
6d3a28a6ce Implement retry logic for UpdateAttributes 2024-03-15 15:18:02 +01:00
3d3fb3c826 Added logout 2024-03-12 20:27:39 +01:00
f52674b179 Fixed dumb routing mistake 2024-03-12 19:56:22 +01:00
697939a17a Added ability to edit user info 2024-03-11 21:08:27 +01:00
f10220f936 Added ability to reject and rework article 2024-03-10 15:03:46 +01:00
a1a6b6c29f Split up db.go into multiple files 2024-03-09 11:06:03 +01:00
42596756de Also missed rss.go 2024-03-09 10:27:55 +01:00
8530c76f2d Missed main when converting to MVC 2024-03-09 10:27:04 +01:00
c6b2a17220 Changed everything to MVC 2024-03-09 10:25:20 +01:00
88e0d5086c Converted RSS feed to be DB based 2024-03-09 10:12:46 +01:00
fa5f189cda Reachieve basic functionality 2024-03-07 20:11:28 +01:00
4d65be195b Articles and tags are now inserted into DB correctly 2024-03-07 15:31:00 +01:00
582f25bec7 Converted articles and tags to DB base 2024-03-06 20:53:17 +01:00
052d36b01b Handle rollbackError with log.Fatalf() 2024-03-06 16:58:41 +01:00
ea45da66b7 Corrected transaction for ChangePassword() 2024-03-06 16:51:08 +01:00
3822a3f30e Use transaction when necessary 2024-03-06 15:37:59 +01:00
f1abb9d353 Load *ArticleList, *Taglist and *Channel correctly 2024-03-06 15:37:42 +01:00
6baaec5b33 Moved main.go into cmd 2024-03-05 18:26:50 +01:00
4aa4fff5e8 Added Tags to RSS feed as categories 2024-03-05 18:20:34 +01:00
a9c61c5a11 Converted RSS package to git.streifling.com/jason/rss 2024-03-05 17:13:59 +01:00
dd50c4f385 A bit of cleaning up 2024-03-05 16:38:18 +01:00
b74036343f Added partial support for tags 2024-03-03 13:56:49 +01:00
45036fe286 Initial sessions implementation 2024-03-03 09:16:49 +01:00
8f7ac979a3 Just a bit of cleaning up 2024-03-02 09:09:55 +01:00
2da17014e4 Created func for minimum spec for rss and article structs, thereby crushing an annoying bug that was caused by not initializing channels but waiting for messages to go through them 2024-03-02 00:28:42 +01:00
4e2cae74bb Changed articles and rss to channels 2024-03-01 21:01:38 +01:00
4b5929911e Implemented proper User struct 2024-03-01 12:25:53 +01:00
f59321b9c6 Added ability to publish articles 2024-03-01 11:30:31 +01:00
cba3c663c9 Added article list for written but non-published articles 2024-02-27 14:10:27 +01:00
59029c86a9 Convert title and description to plain text 2024-02-27 09:03:21 +01:00
8f5739fb68 Implemented hub 2024-02-24 15:31:33 +01:00
49988edd82 Add ability to display feed 2024-02-24 14:49:29 +01:00
36f7a92a06 Added messages and field memory for adding user 2024-02-24 13:25:32 +01:00
f716e9f0b5 Require all fields to be filled out when creating a new user 2024-02-24 12:10:34 +01:00
f3c8cd6fa5 Implemented logging to file 2024-02-24 11:41:01 +01:00
280e88a526 Check if user already exists and bug fix 2024-02-24 10:56:12 +01:00
8ef6b6472d Added ability to add user 2024-02-24 10:28:12 +01:00
2e08600814 Added ability to login 2024-02-24 09:54:25 +01:00
068bf045a7 Check user credentials before adding user 2024-02-22 20:12:09 +01:00
96fe38726c Added ability to update Passwords 2024-02-22 19:27:41 +01:00
75a21eeb9f Added ability to add user 2024-02-22 18:49:51 +01:00
6020b24e44 Changed error messages 2024-02-22 15:23:29 +01:00
ebfe01069c Added HTML sanitizer 2024-02-22 15:22:45 +01:00
5d41543543 Added initial support for MySQL databases 2024-02-18 16:37:13 +01:00
2ccc9c7397 Handle misssed errors for encoding and decoding feeds 2024-02-18 14:31:28 +01:00
c5623fe4fd Added description and a way to save and restore the RSS feed. 2024-02-18 14:01:06 +01:00
ee04a2a351 Create RSS from HTML 2024-02-18 12:41:49 +01:00
aa034701df Show HTML on website 2024-02-18 10:48:37 +01:00
ad9bfb2439 First implementation of web based editor to HTML pipeline 2024-02-18 10:07:49 +01:00
13 changed files with 382 additions and 98 deletions

View File

@ -209,7 +209,7 @@ If you develop a new program, and you want it to be of the greatest possible use
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer to where the full notice is found. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer to where the full notice is found.
cpolis cpolis
Copyright (C) 2024 jason Copyright (C) 2024 Jason Streifling
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
@ -221,7 +221,7 @@ Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode:
cpolis Copyright (C) 2024 jason cpolis Copyright (C) 2024 Jason Streifling
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details.

View File

@ -50,6 +50,7 @@ func main() {
mux.HandleFunc("GET /edit-user/", view.EditUser(db, store)) mux.HandleFunc("GET /edit-user/", view.EditUser(db, store))
mux.HandleFunc("GET /hub/", view.ShowHub(db, store)) mux.HandleFunc("GET /hub/", view.ShowHub(db, store))
mux.HandleFunc("GET /logout/", view.Logout(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 /rejected-articles/", view.ShowRejectedArticles(db, store))
mux.HandleFunc("GET /rss/", view.ShowRSS( mux.HandleFunc("GET /rss/", view.ShowRSS(
db, db,
@ -57,6 +58,7 @@ func main() {
"https://distrikt-ni-st.de", "https://distrikt-ni-st.de",
"Freiheit, Gleichheit, Brüderlichkeit, Toleranz und Humanität", "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 /unpublished-articles/", view.ShowUnpublishedArticles(db))
mux.HandleFunc("GET /write-article/", view.WriteArticle(db)) mux.HandleFunc("GET /write-article/", view.WriteArticle(db))

View File

@ -1,7 +1,10 @@
package model package model
import ( import (
"context"
"database/sql"
"fmt" "fmt"
"log"
"time" "time"
) )
@ -14,26 +17,64 @@ type Article struct {
Rejected bool Rejected bool
ID int64 ID int64
AuthorID int64 AuthorID int64
IssueID int64
} }
func (db *DB) AddArticle(a *Article) (int64, error) { 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 INSERT INTO articles
(title, description, content, published, rejected, author_id) (title, description, content, published, rejected, author_id, issue_id)
VALUES (?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?)
` `
result, err := db.Exec(query, a.Title, a.Description, a.Content, for i := 0; i < TxMaxRetries; i++ {
a.Published, a.Rejected, a.AuthorID) id, err := func() (int64, error) {
tx, err := db.BeginTx(context.Background(), txOptions)
if err != nil { if err != nil {
return 0, fmt.Errorf("error inserting article into DB: %v", err) return 0, fmt.Errorf("error starting transaction: %v", err)
}
id, err := result.LastInsertId()
if err != nil {
return 0, fmt.Errorf("error retrieving last ID: %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 return id, nil
}()
if err == nil {
return id, nil
}
log.Println(err)
wait(i)
}
return 0, fmt.Errorf("error: %v unsuccessful retries for DB operation, aborting", TxMaxRetries)
} }
func (db *DB) GetArticle(id int64) (*Article, error) { func (db *DB) GetArticle(id int64) (*Article, error) {
@ -79,8 +120,8 @@ func (db *DB) GetCertainArticles(published, rejected bool) ([]*Article, error) {
article := new(Article) article := new(Article)
var created []byte var created []byte
if err = rows.Scan(&article.ID, &article.Title, &created, &article.Description, if err = rows.Scan(&article.ID, &article.Title, &created,
&article.Content, &article.AuthorID); err != nil { &article.Description, &article.Content, &article.AuthorID); err != nil {
return nil, fmt.Errorf("error scanning article row: %v", err) 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 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)
}

View File

@ -3,12 +3,14 @@ package model
import ( import (
"fmt" "fmt"
"log" "log"
"math"
"math/rand/v2"
"time"
) )
func (db *DB) WriteArticleTags(articleID int64, tagIDs []int64) error { 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++ { for i := 0; i < TxMaxRetries; i++ {
err := func() error { err := func() error {
tx, err := db.Begin() tx, err := db.Begin()
@ -17,13 +19,9 @@ func (db *DB) WriteArticleTags(articleID int64, tagIDs []int64) error {
} }
for _, tagID := range tagIDs { for _, tagID := range tagIDs {
query := `
INSERT INTO articles_tags (article_id, tag_id)
VALUES (?, ?)
`
if _, err := tx.Exec(query, articleID, tagID); err != nil { if _, err := tx.Exec(query, articleID, tagID); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != 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) 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) log.Println(err)
waitTime := time.Duration(math.Pow(2, float64(i))) * time.Second wait(i)
jitter := time.Duration(rand.IntN(1000)) * time.Millisecond
time.Sleep(waitTime + jitter)
} }
return fmt.Errorf("error: %v unsuccessful retries for DB operation, aborting", TxMaxRetries) 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 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)
}

View File

@ -16,13 +16,14 @@ import (
"golang.org/x/term" "golang.org/x/term"
) )
var TxMaxRetries = 3 var TxMaxRetries = 5
type ( type (
DB struct{ *sql.DB } DB struct{ *sql.DB }
Tx struct{ *sql.Tx } Tx struct{ *sql.Tx }
Attribute struct { Attribute struct {
Value interface{} Value any
Table string Table string
AttName string AttName string
ID int64 ID int64
@ -70,9 +71,15 @@ func getCredentials() (string, string, error) {
return user, pass, nil return user, pass, nil
} }
func wait(iteration int) {
waitTime := time.Duration(math.Pow(2, float64(iteration))) * 100 * time.Millisecond
jitter := time.Duration(rand.IntN(int(waitTime)/2)) * time.Millisecond
time.Sleep(waitTime + jitter)
}
func OpenDB(dbName string) (*DB, error) { func OpenDB(dbName string) (*DB, error) {
var err error var err error
db := DB{DB: &sql.DB{}} db := DB{DB: new(sql.DB)}
cfg := mysql.NewConfig() cfg := mysql.NewConfig()
cfg.DBName = dbName cfg.DBName = dbName
@ -108,7 +115,7 @@ func (db *DB) UpdateAttributes(a ...*Attribute) error {
`, attribute.Table, attribute.AttName) `, attribute.Table, attribute.AttName)
if _, err := tx.Exec(query, attribute.Value, attribute.ID); err != nil { if _, err := tx.Exec(query, attribute.Value, attribute.ID); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != 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 %v in DB: %v", attribute.AttName, err) return fmt.Errorf("error updating %v in DB: %v", attribute.AttName, err)
} }
@ -124,9 +131,7 @@ func (db *DB) UpdateAttributes(a ...*Attribute) error {
} }
log.Println(err) log.Println(err)
waitTime := time.Duration(math.Pow(2, float64(i))) * time.Second wait(i)
jitter := time.Duration(rand.IntN(1000)) * time.Millisecond
time.Sleep(waitTime + jitter)
} }
return fmt.Errorf("error: %v unsuccessful retries for DB operation, aborting", TxMaxRetries) return fmt.Errorf("error: %v unsuccessful retries for DB operation, aborting", TxMaxRetries)
} }
@ -143,31 +148,6 @@ func (db *DB) CountEntries(table string) (int64, error) {
return count, nil return count, nil
} }
func (db *DB) StartTransaction() (*Tx, error) {
tx := &Tx{Tx: new(sql.Tx)}
var err error
tx.Tx, err = db.Begin()
if err != nil {
return nil, fmt.Errorf("error starting transaction: %v", err)
}
return tx, nil
}
func (tx *Tx) CommitTransaction() error {
if err := tx.Commit(); err != nil {
return fmt.Errorf("error committing transaction: %v", err)
}
return nil
}
func (tx *Tx) RollbackTransaction() {
if err := tx.Rollback(); err != nil {
log.Fatalf("error rolling back transaction: %v", err)
}
}
func (tx *Tx) UpdateAttributes(a ...*Attribute) error { func (tx *Tx) UpdateAttributes(a ...*Attribute) error {
for _, attribute := range a { for _, attribute := range a {
query := fmt.Sprintf(` query := fmt.Sprintf(`
@ -177,7 +157,7 @@ func (tx *Tx) UpdateAttributes(a ...*Attribute) error {
`, attribute.Table, attribute.AttName) `, attribute.Table, attribute.AttName)
if _, err := tx.Exec(query, attribute.Value, attribute.ID); err != nil { if _, err := tx.Exec(query, attribute.Value, attribute.ID); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != 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 %v in DB: %v", attribute.AttName, err) return fmt.Errorf("error updating %v in DB: %v", attribute.AttName, err)
} }

66
cmd/model/issues.go Normal file
View 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)
}

View File

@ -3,17 +3,15 @@ package model
import ( import (
"fmt" "fmt"
"log" "log"
"math"
"math/rand/v2"
"time"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
const ( const (
Admin = iota Admin = iota
Publisher
Editor Editor
Writer Author
) )
type User struct { type User struct {
@ -24,21 +22,26 @@ type User struct {
Role int 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) hashedPass, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost)
if err != nil { if err != nil {
return fmt.Errorf("error creating password hash: %v", err) return 0, fmt.Errorf("error creating password hash: %v", err)
} }
query := ` query := `
INSERT INTO users (username, password, first_name, last_name, role) INSERT INTO users (username, password, first_name, last_name, role)
VALUES (?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?)
` `
if _, err = db.Exec(query, user.UserName, string(hashedPass), user.FirstName, user.LastName, user.Role); err != nil { result, err := db.Exec(query, u.UserName, string(hashedPass), u.FirstName, u.LastName, u.Role)
return fmt.Errorf("error inserting user into DB: %v", err) 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) { 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) row := tx.QueryRow(getQuery, id)
if err := row.Scan(&queriedPass); err != nil { if err := row.Scan(&queriedPass); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != 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) return fmt.Errorf("error reading password from DB: %v", err)
} }
if err := bcrypt.CompareHashAndPassword([]byte(queriedPass), []byte(oldPass)); err != nil { if err := bcrypt.CompareHashAndPassword([]byte(queriedPass), []byte(oldPass)); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != 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) 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) newHashedPass, err := bcrypt.GenerateFromPassword([]byte(newPass), bcrypt.DefaultCost)
if err != nil { if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != 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) 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 _, err = tx.Exec(setQuery, string(newHashedPass), id); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != 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) 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) 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) 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 !passwordEmpty {
if err = tx.ChangePassword(id, oldPass, newPass); err != nil { if err = tx.ChangePassword(id, oldPass, newPass); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != 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) 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}, &Attribute{Table: "users", ID: id, AttName: "last_name", Value: last},
); err != nil { ); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != 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) 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) log.Println(err)
waitTime := time.Duration(math.Pow(2, float64(i))) * time.Second wait(i)
jitter := time.Duration(rand.IntN(1000)) * time.Millisecond
time.Sleep(waitTime + jitter)
} }
return fmt.Errorf("error: %v unsuccessful retries for DB operation, aborting", TxMaxRetries) return fmt.Errorf("error: %v unsuccessful retries for DB operation, aborting", TxMaxRetries)

View File

@ -272,6 +272,12 @@ func PublishArticle(db *model.DB, s *control.CookieStore) http.HandlerFunc {
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", msg) 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( if err = db.UpdateAttributes(
&model.Attribute{Table: "articles", ID: id, AttName: "published", Value: true}, &model.Attribute{Table: "articles", ID: id, AttName: "published", Value: true},
&model.Attribute{Table: "articles", ID: id, AttName: "rejected", Value: false}, &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"]) 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)
}
}

View File

@ -1,6 +1,7 @@
package view package view
import ( import (
"fmt"
"html/template" "html/template"
"log" "log"
"net/http" "net/http"
@ -38,6 +39,7 @@ func ShowRSS(db *model.DB, title, link, desc string) http.HandlerFunc {
for _, tag := range tags { for _, tag := range tags {
tagNames = append(tagNames, tag.Name) tagNames = append(tagNames, tag.Name)
} }
tagNames = append(tagNames, fmt.Sprint("Orient Express ", article.IssueID))
user, err := db.GetUser(article.AuthorID) user, err := db.GetUser(article.AuthorID)
if err != nil { if err != nil {

View File

@ -86,19 +86,21 @@ func AddUser(db *model.DB, s *control.CookieStore) http.HandlerFunc {
return return
} }
num, err := db.CountEntries("users") htmlData.ID, err = db.AddUser(htmlData.User, pass)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
if num == 0 {
if htmlData.Role != model.Admin { if htmlData.ID == 1 {
htmlData.Msg = "Der erste Benutzer muss ein Administrator sein."
htmlData.Role = model.Admin htmlData.Role = model.Admin
tmpl, err := template.ParseFiles("web/templates/add-user.html")
tmpl = template.Must(tmpl, err) if err = db.UpdateAttributes(
tmpl.ExecuteTemplate(w, "page-content", htmlData) &model.Attribute{Table: "users", ID: id, AttName: "role", Value: htmlData.Role},
); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
@ -107,13 +109,13 @@ func AddUser(db *model.DB, s *control.CookieStore) http.HandlerFunc {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
}
if err := db.AddUser(htmlData.User, pass); err != nil { if _, err := db.AddIssue(); err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
}
tmpl, err := template.ParseFiles("web/templates/hub.html") tmpl, err := template.ParseFiles("web/templates/hub.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", 0) template.Must(tmpl, err).ExecuteTemplate(w, "page-content", 0)

View File

@ -8,10 +8,12 @@
<input required name="first-name" placeholder="Vorname" type="text" value="{{.FirstName}}" /> <input required name="first-name" placeholder="Vorname" type="text" value="{{.FirstName}}" />
<input required name="last-name" placeholder="Nachname" type="text" value="{{.LastName}}" /> <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}} /> <input required id="author" name="role" type="radio" value="3" {{if eq .Role 3 }}checked{{end}} />
<label for="writer">Schreiber</label> <label for="author">Autor</label>
<input required id="editor" name="role" type="radio" value="1" {{if eq .Role 1 }}checked{{end}} /> <input required id="editor" name="role" type="radio" value="2" {{if eq .Role 2 }}checked{{end}} />
<label for="editor">Redakteur</label> <label for="editor">Redakteur</label>
<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}} /> <input required id="admin" name="role" type="radio" value="0" {{if eq .Role 0 }}checked{{end}} />
<label for="admin">Admin</label> <label for="admin">Admin</label>

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

View File

@ -4,10 +4,13 @@
<button hx-get="/rejected-articles/" hx-target="#page-content">Abgelehnte Artikel</button> <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="/rss/" hx-target="#page-content">RSS Feed</button>
<button hx-get="/edit-user/" hx-target="#page-content">Benutzer bearbeiten</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="/unpublished-articles/" hx-target="#page-content">Unveröffentlichte Artikel</button>
<button hx-get="/create-tag/" hx-target="#page-content">Neuer Tag</button> <button hx-get="/create-tag/" hx-target="#page-content">Neuer Tag</button>
{{end}} {{end}}
{{if lt . 2}}
<button hx-get="/this-issue/" hx-target="#page-content">Diese Ausgabe</button>
{{end}}
{{if eq . 0}} {{if eq . 0}}
<button hx-get="/create-user/" hx-target="#page-content">Benutzer hinzufügen</button> <button hx-get="/create-user/" hx-target="#page-content">Benutzer hinzufügen</button>
{{end}} {{end}}