From 7ec8dbc211ec4c2fcf48d94b8c30f502f933d864 Mon Sep 17 00:00:00 2001 From: Spaenny Date: Sun, 29 Aug 2021 19:26:01 +0200 Subject: [PATCH] added registeration and started user login --- go.mod | 1 + go.sum | 8 ++ goddit.go | 14 +++ migrations/3_create_users_table.down.sql | 1 + migrations/3_create_users_table.up.sql | 5 + postgres/store.go | 2 + postgres/user_store.go | 64 +++++++++++++ templates/layout.html | 2 + templates/user_login.html | 24 +++++ templates/user_register.html | 24 +++++ web/forms.go | 52 ++++++++++ web/handler.go | 8 +- web/sessions.go | 6 ++ web/user_handler.go | 117 +++++++++++++++++++++++ 14 files changed, 327 insertions(+), 1 deletion(-) create mode 100644 migrations/3_create_users_table.down.sql create mode 100644 migrations/3_create_users_table.up.sql create mode 100644 postgres/user_store.go create mode 100644 templates/user_login.html create mode 100644 templates/user_register.html create mode 100644 web/user_handler.go diff --git a/go.mod b/go.mod index 7056015..7a9d0f4 100644 --- a/go.mod +++ b/go.mod @@ -10,4 +10,5 @@ require ( github.com/gorilla/csrf v1.7.1 // indirect github.com/jmoiron/sqlx v1.3.4 // indirect github.com/lib/pq v1.10.2 // indirect + golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect ) diff --git a/go.sum b/go.sum index bb48cb2..a0498ac 100644 --- a/go.sum +++ b/go.sum @@ -33,5 +33,13 @@ github.com/ogier/pflag v0.0.1 h1:RW6JSWSu/RkSatfcLtogGfFgpim5p7ARQ10ECk5O750= github.com/ogier/pflag v0.0.1/go.mod h1:zkFki7tvTa0tafRvTBIZTvzYyAu6kQhPZFnshFFPE+g= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ= +golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/goddit.go b/goddit.go index edb8ff5..b6ba6b1 100644 --- a/goddit.go +++ b/goddit.go @@ -25,6 +25,11 @@ type Comment struct { Votes int `db:"votes"` } +type User struct { + ID uuid.UUID `db:"id"` + Username string `db:"username"` + Password string `db:"password"` +} type ThreadStore interface { Thread(id uuid.UUID) (Thread, error) Threads() ([]Thread, error) @@ -50,8 +55,17 @@ type CommentStore interface { DeleteComment(id uuid.UUID) error } +type UserStore interface { + User(id uuid.UUID) (User, error) + UserByUsername(username string) (User, error) + CreateUser(u *User) error + UpdateUser(u *User) error + DeleteUser(id uuid.UUID) error +} + type Store interface { ThreadStore PostStore CommentStore + UserStore } diff --git a/migrations/3_create_users_table.down.sql b/migrations/3_create_users_table.down.sql new file mode 100644 index 0000000..441087a --- /dev/null +++ b/migrations/3_create_users_table.down.sql @@ -0,0 +1 @@ +DROP TABLE users; \ No newline at end of file diff --git a/migrations/3_create_users_table.up.sql b/migrations/3_create_users_table.up.sql new file mode 100644 index 0000000..2cafcd8 --- /dev/null +++ b/migrations/3_create_users_table.up.sql @@ -0,0 +1,5 @@ +CREATE TABLE users ( + id UUID PRIMARY KEY, + username TEXT NOT NULL UNIQUE, + password TEXT NOT NULL +); \ No newline at end of file diff --git a/postgres/store.go b/postgres/store.go index 980e87a..ce4f946 100644 --- a/postgres/store.go +++ b/postgres/store.go @@ -20,6 +20,7 @@ func NewStore(dataSourceName string) (*Store, error) { ThreadStore: &ThreadStore{DB: db}, PostStore: &PostStore{DB: db}, CommentStore: &CommentStore{DB: db}, + UserStore: &UserStore{DB: db}, }, nil } @@ -27,4 +28,5 @@ type Store struct { *ThreadStore *PostStore *CommentStore + *UserStore } diff --git a/postgres/user_store.go b/postgres/user_store.go new file mode 100644 index 0000000..f4b5af9 --- /dev/null +++ b/postgres/user_store.go @@ -0,0 +1,64 @@ +package postgres + +import ( + "fmt" + + "git.snrd.de/Spaenny/goddit" + "github.com/google/uuid" + "github.com/jmoiron/sqlx" +) + +type UserStore struct { + *sqlx.DB +} + +func (s *UserStore) User(id uuid.UUID) (goddit.User, error) { + var u goddit.User + if err := s.Get(&u, `SELECT * FROM users WHERE id = $1`, id); err != nil { + return goddit.User{}, fmt.Errorf("error getting user: %w", err) + } + return u, nil +} + +func (s *UserStore) UserByUsername(username string) (goddit.User, error) { + var u goddit.User + if err := s.Get(&u, `SELECT * FROM users WHERE username = $1`, username); err != nil { + return goddit.User{}, fmt.Errorf("error getting user: %w", err) + } + return u, nil +} + +func (s *UserStore) Users() ([]goddit.User, error) { + var uu []goddit.User + if err := s.Select(&uu, `SELECT * FROM users`); err != nil { + return []goddit.User{}, fmt.Errorf("error getting users: %w", err) + } + return uu, nil +} + +func (s *UserStore) CreateUser(u *goddit.User) error { + if err := s.Get(u, `INSERT INTO users VALUES($1, $2, $3) RETURNING *`, + u.ID, + u.Username, + u.Password); err != nil { + return fmt.Errorf("error creating user: %w", err) + } + return nil +} + +func (s *UserStore) UpdateUser(u *goddit.User) error { + if err := s.Get(u, `UPDATE INTO users SET username = $1, password = $2 WHERE id = $3) RETURNING *`, + u.Username, + u.Password, + u.ID); err != nil { + return fmt.Errorf("error updating user: %w", err) + } + return nil +} + +func (s *UserStore) DeleteUser(id uuid.UUID) error { + if _, err := s.Exec(`DELETE FROM users WHERE id = $1`, id); err != nil { + return fmt.Errorf("error deleteing user: %w", err) + } + return nil +} diff --git a/templates/layout.html b/templates/layout.html index ba989cb..311ca59 100644 --- a/templates/layout.html +++ b/templates/layout.html @@ -12,6 +12,8 @@
diff --git a/templates/user_login.html b/templates/user_login.html new file mode 100644 index 0000000..3a22265 --- /dev/null +++ b/templates/user_login.html @@ -0,0 +1,24 @@ +{{define "header"}} +

Register

+{{end}} + + {{define "content"}} +
+ {{.CSRF}} +
+ + + {{with .Form.Errors.Username}} +
{{.}}
+ {{end}} +
+
+ + + {{with .Form.Errors.Password}} +
{{.}}
+ {{end}} +
+ +
+ {{end}} diff --git a/templates/user_register.html b/templates/user_register.html new file mode 100644 index 0000000..bc30933 --- /dev/null +++ b/templates/user_register.html @@ -0,0 +1,24 @@ +{{define "header"}} +

Register

+{{end}} + + {{define "content"}} +
+ {{.CSRF}} +
+ + + {{with .Form.Errors.Username}} +
{{.}}
+ {{end}} +
+
+ + + {{with .Form.Errors.Password}} +
{{.}}
+ {{end}} +
+ +
+ {{end}} diff --git a/web/forms.go b/web/forms.go index e524629..d4ee8cd 100644 --- a/web/forms.go +++ b/web/forms.go @@ -6,6 +6,8 @@ func init() { gob.Register(CreatePostForm{}) gob.Register(CreateThreadForm{}) gob.Register(CreateCommentForm{}) + gob.Register(RegisterForm{}) + gob.Register(LoginForm{}) gob.Register(FormErrors{}) } @@ -66,3 +68,53 @@ func (f *CreateCommentForm) Validate() bool { return len(f.Errors) == 0 } + +type RegisterForm struct { + Username string + Password string + UsernameTaken bool + + Errors FormErrors +} + +func (f *RegisterForm) Validate() bool { + f.Errors = FormErrors{} + + if f.Username == "" { + f.Errors["Username"] = "Please enter a username!" + } else if f.UsernameTaken { + f.Errors["Username"] = "This username is already taken!" + } + + if f.Password == "" { + f.Errors["Password"] = "Please enter a password!" + } else if len(f.Password) < 8 { + f.Errors["Password"] = "Your password must be at least 8 charachters long." + } + + return len(f.Errors) == 0 +} + +type LoginForm struct { + Username string + Password string + IncorrectCredentials bool + + Errors FormErrors +} + +func (f *LoginForm) Validate() bool { + f.Errors = FormErrors{} + + if f.Username == "" { + f.Errors["Username"] = "Please enter a username!" + } else if f.IncorrectCredentials { + f.Errors["Username"] = "Username or password is incorrect." + } + + if f.Password == "" { + f.Errors["Password"] = "Please enter a password!" + } + + return len(f.Errors) == 0 +} diff --git a/web/handler.go b/web/handler.go index 8491102..c6653b0 100644 --- a/web/handler.go +++ b/web/handler.go @@ -22,6 +22,7 @@ func NewHandler(store goddit.Store, sessions *scs.SessionManager, csrfKey []byte threads := ThreadHandler{store: store, sessions: sessions} posts := PostHandler{store: store, sessions: sessions} comments := CommentHandler{store: store, sessions: sessions} + users := UserHandler{store: store, sessions: sessions} h.Use(middleware.Logger) h.Use(csrf.Protect(csrfKey, csrf.Secure(false))) @@ -41,6 +42,11 @@ func NewHandler(store goddit.Store, sessions *scs.SessionManager, csrfKey []byte r.Post("/{threadID}/{postID}", comments.Store()) }) h.Get("/comments/{id}/vote", comments.Vote()) + h.Get("/register", users.Register()) + h.Post("/register", users.RegisterSubmit()) + h.Get("/login", users.Login()) + h.Post("/login", users.LoginSubmit()) + h.Get("/logout", users.Logout()) return h } @@ -70,7 +76,7 @@ func (h *Handler) Home() http.HandlerFunc { } once.Do(func() { - h.sessions.Put(r.Context(), "flash", "hello") + h.sessions.Put(r.Context(), "flash", "helloc") }) tmpl.Execute(w, data{ diff --git a/web/sessions.go b/web/sessions.go index 5a7a53e..2ce7b30 100644 --- a/web/sessions.go +++ b/web/sessions.go @@ -3,11 +3,17 @@ package web import ( "context" "database/sql" + "encoding/gob" "github.com/alexedwards/scs/postgresstore" "github.com/alexedwards/scs/v2" + "github.com/google/uuid" ) +func init() { + gob.Register(uuid.UUID{}) +} + func NewSessionsManager(dataSourceName string) (*scs.SessionManager, error) { db, err := sql.Open("postgres", dataSourceName) if err != nil { diff --git a/web/user_handler.go b/web/user_handler.go new file mode 100644 index 0000000..f62399c --- /dev/null +++ b/web/user_handler.go @@ -0,0 +1,117 @@ +package web + +import ( + "html/template" + "net/http" + + "git.snrd.de/Spaenny/goddit" + "github.com/alexedwards/scs/v2" + "github.com/google/uuid" + "github.com/gorilla/csrf" + "golang.org/x/crypto/bcrypt" +) + +type UserHandler struct { + store goddit.Store + sessions *scs.SessionManager +} + +func (h *UserHandler) Register() http.HandlerFunc { + type data struct { + SessionData + + CSRF template.HTML + } + tmpl := template.Must(template.ParseFiles("templates/layout.html", "templates/user_register.html")) + return func(w http.ResponseWriter, r *http.Request) { + tmpl.Execute(w, data{ + SessionData: GetSessionData(h.sessions, r.Context()), + CSRF: csrf.TemplateField(r), + }) + } +} + +func (h *UserHandler) RegisterSubmit() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + form := RegisterForm{ + Username: r.FormValue("username"), + Password: r.FormValue("password"), + UsernameTaken: false, + } + if _, err := h.store.UserByUsername(form.Username); err == nil { + form.UsernameTaken = true + } + if !form.Validate() { + h.sessions.Put(r.Context(), "form", form) + http.Redirect(w, r, r.Referer(), http.StatusFound) + return + } + + password, err := bcrypt.GenerateFromPassword([]byte(form.Password), bcrypt.DefaultCost) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if err := h.store.CreateUser(&goddit.User{ + ID: uuid.New(), + Username: form.Username, + Password: string(password), + }); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + h.sessions.Put(r.Context(), "flash", "Your registration was successful. Please log in.") + http.Redirect(w, r, "/", http.StatusFound) + } +} + +func (h *UserHandler) Login() http.HandlerFunc { + type data struct { + SessionData + + CSRF template.HTML + } + tmpl := template.Must(template.ParseFiles("templates/layout.html", "templates/user_login.html")) + return func(w http.ResponseWriter, r *http.Request) { + tmpl.Execute(w, data{ + SessionData: GetSessionData(h.sessions, r.Context()), + CSRF: csrf.TemplateField(r), + }) + } +} + +func (h *UserHandler) LoginSubmit() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + form := LoginForm{ + Username: r.FormValue("username"), + Password: r.FormValue("password"), + IncorrectCredentials: false, + } + user, err := h.store.UserByUsername(form.Username) + if err == nil { + form.IncorrectCredentials = true + } else { + compareErr := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(form.Password)) + form.IncorrectCredentials = compareErr != nil + } + if !form.Validate() { + h.sessions.Put(r.Context(), "form", form) + http.Redirect(w, r, r.Referer(), http.StatusFound) + return + } + + h.sessions.Put(r.Context(), "user_id", user.ID) + h.sessions.Put(r.Context(), "flash", "You have been logged in successfully.") + http.Redirect(w, r, "/", http.StatusFound) + } +} + +func (h *UserHandler) Logout() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + h.sessions.Remove(r.Context(), "user_id") + h.sessions.Put(r.Context(), "flash", "You have been logged out successfully.") + http.Redirect(w, r, "/", http.StatusFound) + } +}