diff --git a/cmd/data/articles.go b/cmd/data/articles.go index 85c4982..e4ac918 100644 --- a/cmd/data/articles.go +++ b/cmd/data/articles.go @@ -8,12 +8,14 @@ import ( ) type Article struct { - Title string - Created time.Time - Desc string - Content string - Tags []string - UUID uuid.UUID + Title string + Author string + Created time.Time + Desc string + Content string + Tags []string + UUID uuid.UUID + AuthorID int64 } type ArticleList struct { diff --git a/cmd/data/db.go b/cmd/data/db.go index 622b480..ccc13dd 100644 --- a/cmd/data/db.go +++ b/cmd/data/db.go @@ -43,8 +43,7 @@ func (db *DB) AddUser(user User, pass string) error { query := ` INSERT INTO users (username, password, first_name, last_name, role) - VALUES - (?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?) ` _, err = db.Exec(query, user.UserName, string(hashedPass), user.FirstName, user.LastName, user.Role) if err != nil { @@ -58,10 +57,9 @@ func (db *DB) GetID(userName string) (int64, error) { var id int64 query := ` - SELECT id FROM - users - WHERE - username = ? + SELECT id + FROM users + WHERE username = ? ` row := db.QueryRow(query, userName) if err := row.Scan(&id); err != nil { @@ -75,10 +73,9 @@ func (db *DB) CheckPassword(id int64, pass string) error { var queriedPass string query := ` - SELECT password FROM - users - WHERE - id = ? + SELECT password + FROM users + WHERE id = ? ` row := db.QueryRow(query, id) if err := row.Scan(&queriedPass); err != nil { @@ -103,10 +100,9 @@ func (db *DB) ChangePassword(id int64, oldPass, newPass string) error { } query := ` - UPDATE users SET - password = ? - WHERE - id = ? + UPDATE users + SET password = ? + WHERE id = ? ` _, err = db.Exec(query, string(newHashedPass), id) if err != nil { @@ -127,3 +123,21 @@ func (db *DB) CountEntries() (int64, error) { return count, nil } + +// TODO: No need for ID field in general +func (db *DB) GetUser(id int64) (*User, error) { + user := new(User) + query := ` + SELECT id, username, first_name, last_name, role + FROM users + WHERE id = ? + ` + + row := db.QueryRow(query, id) + 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 user, nil +} diff --git a/cmd/data/sessions.go b/cmd/data/sessions.go new file mode 100644 index 0000000..d8458c0 --- /dev/null +++ b/cmd/data/sessions.go @@ -0,0 +1,66 @@ +package data + +import ( + "crypto/rand" + "encoding/gob" + "fmt" + "io" + "os" + + "github.com/gorilla/sessions" +) + +type CookieStore struct { + sessions.CookieStore +} + +func NewKey() ([]byte, error) { + key := make([]byte, 32) + + _, err := io.ReadFull(rand.Reader, key) + if err != nil { + return nil, fmt.Errorf("error generating key: %v", err) + } + + return key, nil +} + +func SaveKey(key []byte, filename string) error { + file, err := os.Create(filename) + if err != nil { + return fmt.Errorf("error creating key file: %v", err) + } + defer file.Close() + file.Chmod(0600) + + encoder := gob.NewEncoder(file) + err = encoder.Encode(key) + if err != nil { + return fmt.Errorf("error ecoding key: %v", err) + } + + return nil +} + +func LoadKey(filename string) ([]byte, error) { + file, err := os.Open(filename) + if err != nil { + return nil, fmt.Errorf("error opening key file: %v", err) + } + + key := make([]byte, 32) + decoder := gob.NewDecoder(file) + err = decoder.Decode(&key) + if err != nil { + return nil, fmt.Errorf("error decoding key: %v", err) + } + + return key, nil +} + +func NewCookieStore(key []byte) *CookieStore { + store := sessions.NewCookieStore(key) + store.Options.Secure = true + store.Options.HttpOnly = true + return &CookieStore{*store} +} diff --git a/cmd/data/user.go b/cmd/data/user.go index 386c27e..6e88e42 100644 --- a/cmd/data/user.go +++ b/cmd/data/user.go @@ -6,13 +6,11 @@ const ( Writer ) -type Role int - type User struct { UserName string FirstName string LastName string RejectedArticles []*Article ID int64 - Role + Role int } diff --git a/cmd/ui/admin.go b/cmd/ui/admin.go index 74b1329..2d43c51 100644 --- a/cmd/ui/admin.go +++ b/cmd/ui/admin.go @@ -38,31 +38,6 @@ func checkUserStrings(user data.User) (string, int, bool) { } } -func HomePage(db *data.DB) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - numRows, err := db.CountEntries() - if err != nil { - log.Fatalln(err) - } - - if numRows == 0 { - files := []string{ - "web/templates/index.html", - "web/templates/add-user.html", - } - tmpl, err := template.ParseFiles(files...) - template.Must(tmpl, err).Execute(w, nil) - } else { - files := []string{ - "web/templates/index.html", - "web/templates/login.html", - } - tmpl, err := template.ParseFiles(files...) - template.Must(tmpl, err).Execute(w, nil) - } - } -} - func CreateUser() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { tmpl, err := template.ParseFiles("web/templates/add-user.html") @@ -84,7 +59,7 @@ func AddUser(db *data.DB) http.HandlerFunc { UserName: r.PostFormValue("username"), FirstName: r.PostFormValue("first-name"), LastName: r.PostFormValue("last-name"), - Role: data.Role(role), + Role: role, }, } pass := r.PostFormValue("password") diff --git a/cmd/ui/articles.go b/cmd/ui/articles.go index 456b7b8..019f342 100644 --- a/cmd/ui/articles.go +++ b/cmd/ui/articles.go @@ -11,21 +11,26 @@ import ( "streifling.com/jason/cpolis/cmd/data" ) -func ShowHub() http.HandlerFunc { +func ShowHub(s *data.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) + } + tmpl, err := template.ParseFiles("web/templates/hub.html") - template.Must(tmpl, err).ExecuteTemplate(w, "page-content", nil) + template.Must(tmpl, err).ExecuteTemplate(w, "page-content", session.Values["role"]) } } -func WriteArticle() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - tmpl, err := template.ParseFiles("web/templates/editor.html") - template.Must(tmpl, err).ExecuteTemplate(w, "page-content", nil) - } +func WriteArticle(w http.ResponseWriter, r *http.Request) { + tmpl, err := template.ParseFiles("web/templates/editor.html") + template.Must(tmpl, err).ExecuteTemplate(w, "page-content", nil) } -func FinishArticle(l *data.ArticleList) http.HandlerFunc { +func FinishArticle(l *data.ArticleList, s *data.CookieStore) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { article := new(data.Article) var err error @@ -51,13 +56,23 @@ func FinishArticle(l *data.ArticleList) http.HandlerFunc { return } + 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) + } + article.UUID = uuid.New() + article.Author = session.Values["name"].(string) article.Created = time.Now() + article.AuthorID = session.Values["id"].(int64) l.Add(article) tmpl, err := template.ParseFiles("web/templates/hub.html") - template.Must(tmpl, err).ExecuteTemplate(w, "page-content", nil) + tmpl = template.Must(tmpl, err) + tmpl.ExecuteTemplate(w, "page-content", session.Values["role"]) } } @@ -68,7 +83,7 @@ func ShowUnpublishedArticles(l *data.ArticleList) http.HandlerFunc { } } -func ReviewArticle(l *data.ArticleList) http.HandlerFunc { +func ReviewArticle(l *data.ArticleList, s *data.CookieStore) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { uuid, err := uuid.Parse(r.PostFormValue("uuid")) if err != nil { @@ -84,12 +99,21 @@ func ReviewArticle(l *data.ArticleList) http.HandlerFunc { return } } + + 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) + } + tmpl, err := template.ParseFiles("web/templates/hub.html") - template.Must(tmpl, err).ExecuteTemplate(w, "page-content", nil) + tmpl = template.Must(tmpl, err) + tmpl.ExecuteTemplate(w, "page-content", session.Values["role"]) } } -func PublishArticle(f *data.Feed, l *data.ArticleList) http.HandlerFunc { +func PublishArticle(f *data.Feed, l *data.ArticleList, s *data.CookieStore) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { uuid, err := uuid.Parse(r.PostFormValue("uuid")) if err != nil { @@ -115,7 +139,15 @@ func PublishArticle(f *data.Feed, l *data.ArticleList) http.HandlerFunc { }) f.Save("tmp/rss.gob") + 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) + } + tmpl, err := template.ParseFiles("web/templates/hub.html") - template.Must(tmpl, err).ExecuteTemplate(w, "page-content", nil) + tmpl = template.Must(tmpl, err) + tmpl.ExecuteTemplate(w, "page-content", session.Values["role"]) } } diff --git a/cmd/ui/session.go b/cmd/ui/session.go deleted file mode 100644 index 7ca3619..0000000 --- a/cmd/ui/session.go +++ /dev/null @@ -1,32 +0,0 @@ -package ui - -import ( - "html/template" - "log" - "net/http" - - "streifling.com/jason/cpolis/cmd/data" -) - -func Login(db *data.DB) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - user := r.PostFormValue("username") - pass := r.PostFormValue("password") - - id, err := db.GetID(user) - if err != nil { - log.Println(err) - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - if err := db.CheckPassword(id, 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", nil) - } -} diff --git a/cmd/ui/sessions.go b/cmd/ui/sessions.go new file mode 100644 index 0000000..a6d2d8c --- /dev/null +++ b/cmd/ui/sessions.go @@ -0,0 +1,83 @@ +package ui + +import ( + "html/template" + "log" + "net/http" + + "streifling.com/jason/cpolis/cmd/data" +) + +func HomePage(db *data.DB, s *data.CookieStore) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + numRows, err := db.CountEntries() + if err != nil { + log.Fatalln(err) + } + + files := []string{"web/templates/index.html"} + if numRows == 0 { + files = append(files, "web/templates/add-user.html") + tmpl, err := template.ParseFiles(files...) + template.Must(tmpl, err).Execute(w, nil) + } else { + session, _ := s.Get(r, "cookie") + if auth, ok := session.Values["authenticated"].(bool); auth && ok { + files = append(files, "web/templates/hub.html") + tmpl, err := template.ParseFiles(files...) + template.Must(tmpl, err).Execute(w, session.Values["role"]) + } else { + files = append(files, "web/templates/login.html") + tmpl, err := template.ParseFiles(files...) + template.Must(tmpl, err).Execute(w, nil) + } + } + } +} + +func Login(db *data.DB, s *data.CookieStore) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + 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) + return + } + + if err := db.CheckPassword(id, password); err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + user, err := db.GetUser(id) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + session, err := s.Get(r, "cookie") + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + session.Values["authenticated"] = true + session.Values["id"] = user.ID + session.Values["name"] = user.FirstName + user.LastName + session.Values["role"] = user.Role + if err := session.Save(r, w); 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", user.Role) + } +} diff --git a/go.mod b/go.mod index cecb8dc..69472b2 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/go-sql-driver/mysql v1.7.1 github.com/google/uuid v1.6.0 github.com/gorilla/feeds v1.1.2 + github.com/gorilla/sessions v1.2.2 github.com/microcosm-cc/bluemonday v1.0.26 github.com/yuin/goldmark v1.7.0 golang.org/x/crypto v0.14.0 @@ -15,6 +16,7 @@ require ( require ( github.com/aymerick/douceur v0.2.0 // indirect github.com/gorilla/css v1.0.0 // indirect + github.com/gorilla/securecookie v1.1.2 // indirect golang.org/x/net v0.17.0 // indirect golang.org/x/sys v0.17.0 // indirect ) diff --git a/go.sum b/go.sum index 57cb739..6c5daec 100644 --- a/go.sum +++ b/go.sum @@ -2,12 +2,18 @@ github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuP github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= github.com/gorilla/feeds v1.1.2 h1:pxzZ5PD3RJdhFH2FsJJ4x6PqMqbgFk1+Vez4XWBW8Iw= github.com/gorilla/feeds v1.1.2/go.mod h1:WMib8uJP3BbY+X8Szd1rA5Pzhdfh+HCCAYT2z7Fza6Y= +github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= +github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= +github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY= +github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= diff --git a/main.go b/main.go index 56eb4d5..7af490e 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "encoding/gob" "log" "net/http" "os" @@ -9,6 +10,10 @@ import ( "streifling.com/jason/cpolis/cmd/ui" ) +func init() { + gob.Register(data.User{}) +} + func main() { logFile, err := os.Create("tmp/cpolis.log") if err != nil { @@ -31,24 +36,33 @@ func main() { "Freiheit, Gleichheit, Brüderlichkeit, Toleranz und Humanität") } + key, err := data.LoadKey("tmp/key.gob") + if err != nil { + key, err = data.NewKey() + if err != nil { + log.Fatalln(err) + } + data.SaveKey(key, "tmp/key.gob") + } + store := data.NewCookieStore(key) + articleList := data.NewArticleList() mux := http.NewServeMux() mux.Handle("/web/static/", http.StripPrefix("/web/static/", http.FileServer(http.Dir("web/static/")))) + mux.HandleFunc("/", ui.HomePage(db, store)) - mux.HandleFunc("/", ui.HomePage(db)) - mux.HandleFunc("/rss/", ui.ShowRSS(feed)) - - mux.HandleFunc("GET /hub/", ui.ShowHub()) + mux.HandleFunc("GET /hub/", ui.ShowHub(store)) + mux.HandleFunc("GET /rss/", ui.ShowRSS(feed)) mux.HandleFunc("POST /add-user/", ui.AddUser(db)) mux.HandleFunc("POST /create-user/", ui.CreateUser()) - mux.HandleFunc("POST /finish-article/", ui.FinishArticle(articleList)) - mux.HandleFunc("POST /login/", ui.Login(db)) - mux.HandleFunc("POST /review-article/", ui.ReviewArticle(articleList)) - mux.HandleFunc("POST /publish-article/", ui.PublishArticle(feed, articleList)) + mux.HandleFunc("POST /finish-article/", ui.FinishArticle(articleList, store)) + mux.HandleFunc("POST /login/", ui.Login(db, store)) + mux.HandleFunc("POST /review-article/", ui.ReviewArticle(articleList, store)) + mux.HandleFunc("POST /publish-article/", ui.PublishArticle(feed, articleList, store)) mux.HandleFunc("POST /unpublished-articles/", ui.ShowUnpublishedArticles(articleList)) - mux.HandleFunc("POST /write-article/", ui.WriteArticle()) + mux.HandleFunc("POST /write-article/", ui.WriteArticle) log.Fatalln(http.ListenAndServe(":8080", mux)) } diff --git a/web/templates/hub.html b/web/templates/hub.html index 2161767..ad55008 100644 --- a/web/templates/hub.html +++ b/web/templates/hub.html @@ -2,6 +2,10 @@