added registeration and started user login

This commit is contained in:
Philipp 2021-08-29 19:26:01 +02:00
parent 3f2cbcea33
commit 7ec8dbc211
No known key found for this signature in database
GPG Key ID: 276B613AF9DBE9C3
14 changed files with 327 additions and 1 deletions

1
go.mod
View File

@ -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
)

8
go.sum
View File

@ -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=

View File

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

View File

@ -0,0 +1 @@
DROP TABLE users;

View File

@ -0,0 +1,5 @@
CREATE TABLE users (
id UUID PRIMARY KEY,
username TEXT NOT NULL UNIQUE,
password TEXT NOT NULL
);

View File

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

64
postgres/user_store.go Normal file
View File

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

View File

@ -12,6 +12,8 @@
<body>
<nav class="navbar navbar-light container">
<a class="navbar-brand text-primary" href="/">goddit</a>
<div class="flex-fill"></div>
<a class="text-primary text-primary" href="/register">Register</a>
</nav>
<div class="header bg-light border-bottom border-top py-5">
<div class="container">

24
templates/user_login.html Normal file
View File

@ -0,0 +1,24 @@
{{define "header"}}
<h1 class="mb-0">Register</h1>
{{end}}
{{define "content"}}
<form action="/login" method="POST">
{{.CSRF}}
<div class="form-group">
<label>Username</label>
<input name="username" type="text" class="form-control {{with .Form.Errors.Title}}is-invalid{{end}}" placeholder="Enter your username" value="{{with .Form.Username}}{{.}}{{end}}">
{{with .Form.Errors.Username}}
<div class="invalid-feedack">{{.}}</div>
{{end}}
</div>
<div class="form-group">
<label>Password</label>
<input name="password" type="password" class="form-control {{with .Form.Errors.Password}}is-invalid{{end}}" placeholder="Enter your password" value="{{with .Form.Password}}{{.}}{{end}}">
{{with .Form.Errors.Password}}
<div class="invalid-feedack">{{.}}</div>
{{end}}
</div>
<button type="submit" class="btn btn-primary">Login</button>
</form>
{{end}}

View File

@ -0,0 +1,24 @@
{{define "header"}}
<h1 class="mb-0">Register</h1>
{{end}}
{{define "content"}}
<form action="/register" method="POST">
{{.CSRF}}
<div class="form-group">
<label>Username</label>
<input name="username" type="text" class="form-control {{with .Form.Errors.Title}}is-invalid{{end}}" placeholder="Choose a username" value="{{with .Form.Username}}{{.}}{{end}}">
{{with .Form.Errors.Username}}
<div class="invalid-feedack">{{.}}</div>
{{end}}
</div>
<div class="form-group">
<label>Password</label>
<input name="password" type="password" class="form-control {{with .Form.Errors.Password}}is-invalid{{end}}" placeholder="Please enter your password" value="{{with .Form.Password}}{{.}}{{end}}">
{{with .Form.Errors.Password}}
<div class="invalid-feedack">{{.}}</div>
{{end}}
</div>
<button type="submit" class="btn btn-primary">Register</button>
</form>
{{end}}

View File

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

View File

@ -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{

View File

@ -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 {

117
web/user_handler.go Normal file
View File

@ -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)
}
}