From 697939a17ab78979c76c39cd09f83d54fca9388f Mon Sep 17 00:00:00 2001 From: Jason Streifling Date: Mon, 11 Mar 2024 21:08:27 +0100 Subject: [PATCH] Added ability to edit user info --- cmd/main.go | 1 + cmd/model/articles.go | 11 ++ cmd/model/db.go | 54 +++++++++ cmd/model/structs.go | 42 ------- cmd/model/tags.go | 5 + cmd/model/users.go | 31 ++--- cmd/view/admin.go | 128 -------------------- cmd/view/articles.go | 16 ++- cmd/view/sessions.go | 7 +- cmd/view/users.go | 219 +++++++++++++++++++++++++++++++++++ web/templates/edit-user.html | 13 +++ web/templates/hub.html | 1 + 12 files changed, 337 insertions(+), 191 deletions(-) delete mode 100644 cmd/model/structs.go delete mode 100644 cmd/view/admin.go create mode 100644 cmd/view/users.go create mode 100644 web/templates/edit-user.html diff --git a/cmd/main.go b/cmd/main.go index a4061a2..32410bc 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -47,6 +47,7 @@ func main() { mux.HandleFunc("GET /create-tag/", view.CreateTag) mux.HandleFunc("GET /create-user/", view.CreateUser) + mux.HandleFunc("GET /edit-user/", view.EditUser(db, store)) mux.HandleFunc("GET /hub/", view.ShowHub(db, store)) mux.HandleFunc("GET /rejected-articles/", view.ShowRejectedArticles(db, store)) mux.HandleFunc("GET /rss/", view.ShowRSS( diff --git a/cmd/model/articles.go b/cmd/model/articles.go index 6c4f75c..43e0e50 100644 --- a/cmd/model/articles.go +++ b/cmd/model/articles.go @@ -5,6 +5,17 @@ import ( "time" ) +type Article struct { + Title string + Created time.Time + Description string + Content string + Published bool + Rejected bool + ID int64 + AuthorID int64 +} + func (db *DB) AddArticle(a *Article) (int64, error) { query := ` INSERT INTO articles diff --git a/cmd/model/db.go b/cmd/model/db.go index 6b69788..566bc95 100644 --- a/cmd/model/db.go +++ b/cmd/model/db.go @@ -17,6 +17,17 @@ type DB struct { *sql.DB } +type Tx struct { + *sql.Tx +} + +type Attribute struct { + Value interface{} + Table string + AttName string + ID int64 +} + func getUsername() (string, error) { user := os.Getenv("DB_USER") if user == "" { @@ -117,3 +128,46 @@ func (db *DB) CountEntries(table string) (int64, error) { 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 { + for _, attribute := range a { + query := fmt.Sprintf(` + UPDATE %s + SET %s = ? + WHERE id = ? + `, attribute.Table, attribute.AttName) + if _, err := tx.Exec(query, attribute.Value, attribute.ID); err != nil { + if rollbackErr := tx.Rollback(); rollbackErr != nil { + log.Fatalf("error: transaction error: %v, rollback error: %v", err, rollbackErr) + } + return fmt.Errorf("error updating article in DB: %v", err) + } + } + + return nil +} diff --git a/cmd/model/structs.go b/cmd/model/structs.go deleted file mode 100644 index 51bac18..0000000 --- a/cmd/model/structs.go +++ /dev/null @@ -1,42 +0,0 @@ -package model - -import ( - "time" -) - -const ( - Admin = iota - Editor - Writer -) - -type User struct { - UserName string - FirstName string - LastName string - ID int64 - Role int -} - -type Tag struct { - Name string - ID int64 -} - -type Article struct { - Title string - Created time.Time - Description string - Content string - Published bool - Rejected bool - ID int64 - AuthorID int64 -} - -type Attribute struct { - Value interface{} - Table string - AttName string - ID int64 -} diff --git a/cmd/model/tags.go b/cmd/model/tags.go index d4a80a9..88e5a37 100644 --- a/cmd/model/tags.go +++ b/cmd/model/tags.go @@ -2,6 +2,11 @@ package model import "fmt" +type Tag struct { + Name string + ID int64 +} + func (db *DB) AddTag(tagName string) error { query := "INSERT INTO tags (name) VALUES (?)" if _, err := db.Exec(query, tagName); err != nil { diff --git a/cmd/model/users.go b/cmd/model/users.go index dbe5888..f0b2802 100644 --- a/cmd/model/users.go +++ b/cmd/model/users.go @@ -7,6 +7,20 @@ import ( "golang.org/x/crypto/bcrypt" ) +const ( + Admin = iota + Editor + Writer +) + +type User struct { + UserName string + FirstName string + LastName string + ID int64 + Role int +} + func (db *DB) AddUser(user *User, pass string) error { hashedPass, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost) if err != nil { @@ -25,7 +39,7 @@ func (db *DB) AddUser(user *User, pass string) error { return nil } -func (db *DB) GetID(userName string) (int64, error) { +func (db *DB) GetID(userName string) (int64, bool) { var id int64 query := ` @@ -35,10 +49,10 @@ func (db *DB) GetID(userName string) (int64, error) { ` row := db.QueryRow(query, userName) if err := row.Scan(&id); err != nil { - return 0, fmt.Errorf("user not in DB: %v", err) + return 0, false } - return id, nil + return id, true } func (db *DB) CheckPassword(id int64, pass string) error { @@ -61,12 +75,7 @@ func (db *DB) CheckPassword(id int64, pass string) error { return nil } -func (db *DB) ChangePassword(id int64, oldPass, newPass string) error { - tx, err := db.Begin() - if err != nil { - return fmt.Errorf("error starting transaction: %v", err) - } - +func (tx *Tx) ChangePassword(id int64, oldPass, newPass string) error { var queriedPass string getQuery := ` SELECT password @@ -108,10 +117,6 @@ func (db *DB) ChangePassword(id int64, oldPass, newPass string) error { return fmt.Errorf("error updating password in DB: %v", err) } - if err = tx.Commit(); err != nil { - return fmt.Errorf("error committing transaction: %v", err) - } - return nil } diff --git a/cmd/view/admin.go b/cmd/view/admin.go deleted file mode 100644 index b94a222..0000000 --- a/cmd/view/admin.go +++ /dev/null @@ -1,128 +0,0 @@ -package view - -import ( - "fmt" - "html/template" - "log" - "net/http" - "strconv" - - "streifling.com/jason/cpolis/cmd/control" - "streifling.com/jason/cpolis/cmd/model" -) - -type AddUserData struct { - *model.User - Msg string -} - -func inputsEmpty(user *model.User, pass, pass2 string) bool { - return len(user.UserName) == 0 || - len(user.FirstName) == 0 || - len(user.LastName) == 0 || - len(pass) == 0 || - len(pass2) == 0 -} - -func checkUserStrings(user *model.User) (string, int, bool) { - userLen := 15 - nameLen := 50 - - if len(user.UserName) > userLen { - return "Benutzername", userLen, false - } else if len(user.FirstName) > nameLen { - return "Vorname", nameLen, false - } else if len(user.LastName) > nameLen { - return "Nachname", nameLen, false - } else { - return "", 0, true - } -} - -func CreateUser(w http.ResponseWriter, r *http.Request) { - tmpl, err := template.ParseFiles("web/templates/add-user.html") - template.Must(tmpl, err).ExecuteTemplate(w, "page-content", nil) -} - -func AddUser(db *model.DB, s *control.CookieStore) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - role, err := strconv.Atoi(r.PostFormValue("role")) - if err != nil { - log.Println(err) - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - htmlData := AddUserData{ - User: &model.User{ - UserName: r.PostFormValue("username"), - FirstName: r.PostFormValue("first-name"), - LastName: r.PostFormValue("last-name"), - Role: role, - }, - } - pass := r.PostFormValue("password") - pass2 := r.PostFormValue("password2") - - if inputsEmpty(htmlData.User, pass, pass2) { - htmlData.Msg = "Alle Felder müssen ausgefüllt werden." - tmpl, err := template.ParseFiles("web/templates/add-user.html") - template.Must(tmpl, err).ExecuteTemplate(w, "page-content", htmlData) - return - } - userString, stringLen, ok := checkUserStrings(htmlData.User) - if !ok { - htmlData.Msg = fmt.Sprint(userString, " ist zu lang. Maximal ", - stringLen, " Zeichen erlaubt.") - tmpl, err := template.ParseFiles("web/templates/add-user.html") - template.Must(tmpl, err).ExecuteTemplate(w, "page-content", htmlData) - return - } - id, _ := db.GetID(htmlData.UserName) - if id != 0 { - htmlData.Msg = fmt.Sprint(htmlData.UserName, - " ist bereits vergeben. Bitte anderen Benutzernamen wählen.") - tmpl, err := template.ParseFiles("web/templates/add-user.html") - template.Must(tmpl, err).ExecuteTemplate(w, "page-content", htmlData) - return - } - if pass != pass2 { - htmlData.Msg = "Die Passwörter stimmen nicht überein." - tmpl, err := template.ParseFiles("web/templates/add-user.html") - template.Must(tmpl, err).ExecuteTemplate(w, "page-content", htmlData) - return - } - - num, err := db.CountEntries("users") - 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) - return - } - - if err := saveSession(w, r, s, htmlData.User); err != nil { - log.Println(err) - 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 - } - - tmpl, err := template.ParseFiles("web/templates/hub.html") - template.Must(tmpl, err).ExecuteTemplate(w, "page-content", 0) - } -} diff --git a/cmd/view/articles.go b/cmd/view/articles.go index 8f6dc85..5626489 100644 --- a/cmd/view/articles.go +++ b/cmd/view/articles.go @@ -213,10 +213,14 @@ func PublishArticle(db *model.DB, s *control.CookieStore) http.HandlerFunc { template.Must(tmpl, err).ExecuteTemplate(w, "page-content", msg) } - db.UpdateAttributes( + if err = db.UpdateAttributes( &model.Attribute{Table: "articles", ID: id, AttName: "published", Value: true}, &model.Attribute{Table: "articles", ID: id, AttName: "rejected", Value: false}, - ) + ); err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } tmpl, err := template.ParseFiles("web/templates/hub.html") tmpl = template.Must(tmpl, err) @@ -240,9 +244,13 @@ func RejectArticle(db *model.DB, s *control.CookieStore) http.HandlerFunc { template.Must(tmpl, err).ExecuteTemplate(w, "page-content", msg) } - db.UpdateAttributes( + if err = db.UpdateAttributes( &model.Attribute{Table: "articles", ID: id, AttName: "rejected", Value: true}, - ) + ); err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } tmpl, err := template.ParseFiles("web/templates/hub.html") tmpl = template.Must(tmpl, err) diff --git a/cmd/view/sessions.go b/cmd/view/sessions.go index a7bfc72..0098147 100644 --- a/cmd/view/sessions.go +++ b/cmd/view/sessions.go @@ -59,10 +59,9 @@ func Login(db *model.DB, s *control.CookieStore) http.HandlerFunc { userName := r.PostFormValue("username") password := r.PostFormValue("password") - id, err := db.GetID(userName) - if err != nil { - log.Println(err) - http.Error(w, err.Error(), http.StatusInternalServerError) + id, ok := db.GetID(userName) + if !ok { + http.Error(w, fmt.Sprintf("no such user: %v", userName), http.StatusInternalServerError) return } diff --git a/cmd/view/users.go b/cmd/view/users.go new file mode 100644 index 0000000..d1d4000 --- /dev/null +++ b/cmd/view/users.go @@ -0,0 +1,219 @@ +package view + +import ( + "fmt" + "html/template" + "log" + "net/http" + "strconv" + + "streifling.com/jason/cpolis/cmd/control" + "streifling.com/jason/cpolis/cmd/model" +) + +type UserData struct { + *model.User + Msg string +} + +func checkUserStrings(user *model.User) (string, int, bool) { + userLen := 15 + nameLen := 50 + + if len(user.UserName) > userLen { + return "Benutzername", userLen, false + } else if len(user.FirstName) > nameLen { + return "Vorname", nameLen, false + } else if len(user.LastName) > nameLen { + return "Nachname", nameLen, false + } else { + return "", 0, true + } +} + +func CreateUser(w http.ResponseWriter, r *http.Request) { + tmpl, err := template.ParseFiles("web/templates/add-user.html") + template.Must(tmpl, err).ExecuteTemplate(w, "page-content", nil) +} + +func AddUser(db *model.DB, s *control.CookieStore) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + role, err := strconv.Atoi(r.PostFormValue("role")) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + htmlData := UserData{ + User: &model.User{ + UserName: r.PostFormValue("username"), + FirstName: r.PostFormValue("first-name"), + LastName: r.PostFormValue("last-name"), + Role: role, + }, + } + pass := r.PostFormValue("password") + pass2 := r.PostFormValue("password2") + + if len(htmlData.UserName) == 0 || len(htmlData.FirstName) == 0 || + len(htmlData.LastName) == 0 || len(pass) == 0 || len(pass2) == 0 { + htmlData.Msg = "Alle Felder müssen ausgefüllt werden." + tmpl, err := template.ParseFiles("web/templates/add-user.html") + template.Must(tmpl, err).ExecuteTemplate(w, "page-content", htmlData) + return + } + userString, stringLen, ok := checkUserStrings(htmlData.User) + if !ok { + htmlData.Msg = fmt.Sprint(userString, " ist zu lang. Maximal ", + stringLen, " Zeichen erlaubt.") + tmpl, err := template.ParseFiles("web/templates/add-user.html") + template.Must(tmpl, err).ExecuteTemplate(w, "page-content", htmlData) + return + } + id, _ := db.GetID(htmlData.UserName) + if id != 0 { + htmlData.Msg = fmt.Sprint(htmlData.UserName, + " ist bereits vergeben. Bitte anderen Benutzernamen wählen.") + tmpl, err := template.ParseFiles("web/templates/add-user.html") + template.Must(tmpl, err).ExecuteTemplate(w, "page-content", htmlData) + return + } + if pass != pass2 { + htmlData.Msg = "Die Passwörter stimmen nicht überein." + tmpl, err := template.ParseFiles("web/templates/add-user.html") + template.Must(tmpl, err).ExecuteTemplate(w, "page-content", htmlData) + return + } + + num, err := db.CountEntries("users") + 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) + return + } + + if err := saveSession(w, r, s, htmlData.User); err != nil { + log.Println(err) + 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 + } + + tmpl, err := template.ParseFiles("web/templates/hub.html") + template.Must(tmpl, err).ExecuteTemplate(w, "page-content", 0) + } +} + +func EditUser(db *model.DB, s *control.CookieStore) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + session, err := s.Get(r, "cookie") + if err != nil { + tmpl, err := template.ParseFiles("web/templates/login.html") + msg := "Session nicht mehr gültig. Bitte erneut anmelden." + template.Must(tmpl, err).ExecuteTemplate(w, "page-content", msg) + } + + userData := UserData{ + User: &model.User{ + ID: session.Values["id"].(int64), + UserName: r.PostFormValue("username"), + FirstName: r.PostFormValue("first-name"), + LastName: r.PostFormValue("last-name"), + }, + } + oldPass := r.PostFormValue("old-password") + newPass := r.PostFormValue("password") + newPass2 := r.PostFormValue("password2") + + if len(userData.UserName) == 0 || len(userData.FirstName) == 0 || + len(userData.LastName) == 0 { + userData.Msg = "Alle Felder mit * müssen ausgefüllt sein." + tmpl, err := template.ParseFiles("web/templates/edit-user.html") + tmpl = template.Must(tmpl, err) + tmpl.ExecuteTemplate(w, "page-content", userData.Msg) + return + } + + userString, stringLen, ok := checkUserStrings(userData.User) + if !ok { + userData.Msg = fmt.Sprint(userString, " ist zu lang. Maximal ", + stringLen, " Zeichen erlaubt.") + tmpl, err := template.ParseFiles("web/templates/edit-user.html") + tmpl = template.Must(tmpl, err) + tmpl.ExecuteTemplate(w, "page-content", userData) + return + } + + if _, ok := db.GetID(userData.UserName); ok { + userData.Msg = "Benutzername bereits vergeben." + userData.UserName = "" + tmpl, err := template.ParseFiles("web/templates/edit-user.html") + tmpl = template.Must(tmpl, err) + tmpl.ExecuteTemplate(w, "page-content", userData) + return + } + + tx, err := db.StartTransaction() + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if len(newPass) > 0 || len(newPass2) > 0 { + if newPass != newPass2 { + tx.RollbackTransaction() + userData.Msg = "Die Passwörter stimmen nicht überein." + tmpl, err := template.ParseFiles("web/templates/edit-user.html") + tmpl = template.Must(tmpl, err) + tmpl.ExecuteTemplate(w, "page-content", userData) + return + } + + if err = tx.ChangePassword(userData.ID, oldPass, newPass); err != nil { + log.Println(err) + userData.Msg = "Das alte Passwort ist nicht korrekt." + tmpl, err := template.ParseFiles("web/templates/edit-user.html") + tmpl = template.Must(tmpl, err) + tmpl.ExecuteTemplate(w, "page-content", userData) + return + } + } + + if err = tx.UpdateAttributes( + &model.Attribute{Table: "users", ID: userData.ID, AttName: "username", Value: userData.UserName}, + &model.Attribute{Table: "users", ID: userData.ID, AttName: "first-name", Value: userData.FirstName}, + &model.Attribute{Table: "users", ID: userData.ID, AttName: "last-name", Value: userData.LastName}, + ); err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if err = tx.Commit(); err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + tmpl, err := template.ParseFiles("web/templates/hub.html") + tmpl = template.Must(tmpl, err) + tmpl.ExecuteTemplate(w, "page-content", session.Values["Role"].(string)) + } +} diff --git a/web/templates/edit-user.html b/web/templates/edit-user.html new file mode 100644 index 0000000..71a6bec --- /dev/null +++ b/web/templates/edit-user.html @@ -0,0 +1,13 @@ +{{define "page-content"}} +
+ + + + + + + + + +
+{{end}} diff --git a/web/templates/hub.html b/web/templates/hub.html index 81bd86e..f7cffc1 100644 --- a/web/templates/hub.html +++ b/web/templates/hub.html @@ -3,6 +3,7 @@ + {{if lt . 2}}