Roughed in verify user. Will resist after restructuring users routes
This commit is contained in:
		
							parent
							
								
									e6733b2c2e
								
							
						
					
					
						commit
						05ded17aec
					
				
					 7 changed files with 126 additions and 7 deletions
				
			
		|  | @ -23,6 +23,7 @@ type Claims struct { | ||||||
| 	Role string | 	Role string | ||||||
| 	Iat  int64 | 	Iat  int64 | ||||||
| 	Exp  int64 | 	Exp  int64 | ||||||
|  | 	Ref  string | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func Handler() http.Handler { | func Handler() http.Handler { | ||||||
|  | @ -39,6 +40,7 @@ func Handler() http.Handler { | ||||||
| 			"role": user.Role, | 			"role": user.Role, | ||||||
| 			"iat":  currentTime.Unix(), | 			"iat":  currentTime.Unix(), | ||||||
| 			"exp":  currentTime.Add(time.Minute * 60 * 24).Unix(), | 			"exp":  currentTime.Add(time.Minute * 60 * 24).Unix(), | ||||||
|  | 			"ref":  "", | ||||||
| 		}, nil | 		}, nil | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -78,6 +80,7 @@ func Handler() http.Handler { | ||||||
| 	// Non-auth routes | 	// Non-auth routes | ||||||
| 	m.Handle("/authenticate", tokenHandler(j.GenerateToken())).Methods("POST") | 	m.Handle("/authenticate", tokenHandler(j.GenerateToken())).Methods("POST") | ||||||
| 	m.Handle("/users", errorHandler(handleCreater(userService))).Methods("POST") | 	m.Handle("/users", errorHandler(handleCreater(userService))).Methods("POST") | ||||||
|  | 	m.Handle("/users/verify/{Nonce}", http.HandlerFunc(handleUserVerify)).Methods("GET") | ||||||
| 
 | 
 | ||||||
| 	// Auth routes | 	// Auth routes | ||||||
| 	m.Handle("/users", j.Secure(errorHandler(handleLister(userService)), verifyClaims)).Methods("GET") | 	m.Handle("/users", j.Secure(errorHandler(handleLister(userService)), verifyClaims)).Methods("GET") | ||||||
|  |  | ||||||
							
								
								
									
										11
									
								
								helpers.go
									
										
									
									
									
								
							
							
						
						
									
										11
									
								
								helpers.go
									
										
									
									
									
								
							|  | @ -1,6 +1,8 @@ | ||||||
| package main | package main | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"crypto/rand" | ||||||
|  | 	"encoding/base64" | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net/http" | 	"net/http" | ||||||
|  | @ -89,3 +91,12 @@ func verifyPassword(s string) (sevenOrMore, number, upper bool) { | ||||||
| 	sevenOrMore = letters >= 7 | 	sevenOrMore = letters >= 7 | ||||||
| 	return | 	return | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func generateNonce() (string, error) { | ||||||
|  | 	b := make([]byte, 32) | ||||||
|  | 	_, err := rand.Read(b) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  | 	return base64.URLEncoding.EncodeToString(b), nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -1,6 +1,10 @@ | ||||||
| -- bactdb | -- bactdb | ||||||
| -- Matthew R Dillon | -- Matthew R Dillon | ||||||
| 
 | 
 | ||||||
|  | -- Need to include something to keep gomigrate happy. | ||||||
|  | -- SELECT 1; | ||||||
|  | 
 | ||||||
| DROP TABLE users; | DROP TABLE users; | ||||||
| 
 | 
 | ||||||
| DROP TYPE e_roles; | DROP TYPE e_roles; | ||||||
|  | 
 | ||||||
|  |  | ||||||
|  | @ -1,17 +1,23 @@ | ||||||
| -- bactdb | -- bactdb | ||||||
| -- Matthew R Dillon | -- Matthew R Dillon | ||||||
| 
 | 
 | ||||||
| CREATE TYPE e_roles AS ENUM('R', 'W', 'A'); | DO $$ | ||||||
| -- 'R': read-only, default | BEGIN | ||||||
| -- 'W': read-write |     IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'e_roles') THEN | ||||||
| -- 'A': administrator |         CREATE TYPE e_roles AS ENUM('R', 'W', 'A'); | ||||||
|  |         -- 'R': read-only, default | ||||||
|  |         -- 'W': read-write | ||||||
|  |         -- 'A': administrator | ||||||
|  |     END IF; | ||||||
|  | END$$; | ||||||
| 
 | 
 | ||||||
| CREATE TABLE users ( | CREATE TABLE IF NOT EXISTS users ( | ||||||
|     id BIGSERIAL NOT NULL, |     id BIGSERIAL NOT NULL, | ||||||
|     email CHARACTER VARYING(254) NOT NULL UNIQUE, |     email CHARACTER VARYING(254) NOT NULL UNIQUE, | ||||||
|     password CHARACTER(60) NOT NULL, |     password CHARACTER(60) NOT NULL, | ||||||
|     name TEXT NOT NULL, |     name TEXT NOT NULL, | ||||||
|     role e_roles DEFAULT 'R' NOT NULL, |     role e_roles DEFAULT 'R' NOT NULL, | ||||||
|  |     verified BOOLEAN NOT NULL DEFAULT FALSE, | ||||||
| 
 | 
 | ||||||
|     created_at TIMESTAMP WITH TIME ZONE NOT NULL, |     created_at TIMESTAMP WITH TIME ZONE NOT NULL, | ||||||
|     updated_at TIMESTAMP WITH TIME ZONE NOT NULL, |     updated_at TIMESTAMP WITH TIME ZONE NOT NULL, | ||||||
|  |  | ||||||
							
								
								
									
										5
									
								
								migrations/00011_AddVerification_down.sql
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								migrations/00011_AddVerification_down.sql
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | ||||||
|  | -- bactdb | ||||||
|  | -- Matthew R Dillon | ||||||
|  | 
 | ||||||
|  | DROP TABLE verification; | ||||||
|  | 
 | ||||||
							
								
								
									
										13
									
								
								migrations/00011_AddVerification_up.sql
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								migrations/00011_AddVerification_up.sql
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,13 @@ | ||||||
|  | -- bactdb | ||||||
|  | -- Matthew R Dillon | ||||||
|  | 
 | ||||||
|  | CREATE TABLE verification ( | ||||||
|  |     user_id BIGINT NOT NULL, | ||||||
|  |     nonce CHARACTER(60) NOT NULL UNIQUE, | ||||||
|  | 
 | ||||||
|  |     created_at TIMESTAMP WITH TIME ZONE NOT NULL, | ||||||
|  | 
 | ||||||
|  |     CONSTRAINT verification_pkey PRIMARY KEY (user_id), | ||||||
|  |     FOREIGN KEY (user_id) REFERENCES users(id) | ||||||
|  | ); | ||||||
|  | 
 | ||||||
							
								
								
									
										81
									
								
								users.go
									
										
									
									
									
								
							
							
						
						
									
										81
									
								
								users.go
									
										
									
									
									
								
							|  | @ -4,10 +4,15 @@ import ( | ||||||
| 	"database/sql" | 	"database/sql" | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"errors" | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"log" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"net/url" | 	"net/url" | ||||||
| 	"regexp" | 	"regexp" | ||||||
| 
 | 
 | ||||||
|  | 	"github.com/gorilla/mux" | ||||||
|  | 	"github.com/lib/pq" | ||||||
|  | 
 | ||||||
| 	"golang.org/x/crypto/bcrypt" | 	"golang.org/x/crypto/bcrypt" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -17,6 +22,8 @@ var ( | ||||||
| 	ErrUserNotUpdated         = errors.New("User not updated") | 	ErrUserNotUpdated         = errors.New("User not updated") | ||||||
| 	ErrUserNotUpdatedJSON     = newJSONError(ErrUserNotUpdated, http.StatusBadRequest) | 	ErrUserNotUpdatedJSON     = newJSONError(ErrUserNotUpdated, http.StatusBadRequest) | ||||||
| 	ErrInvalidEmailOrPassword = errors.New("Invalid email or password") | 	ErrInvalidEmailOrPassword = errors.New("Invalid email or password") | ||||||
|  | 	ErrEmailAddressTaken      = errors.New("Email address already registered") | ||||||
|  | 	ErrEmailAddressTakenJSON  = newJSONError(ErrEmailAddressTaken, http.StatusBadRequest) | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func init() { | func init() { | ||||||
|  | @ -31,6 +38,7 @@ type User struct { | ||||||
| 	Password  string   `db:"password" json:"password,omitempty"` | 	Password  string   `db:"password" json:"password,omitempty"` | ||||||
| 	Name      string   `db:"name" json:"name"` | 	Name      string   `db:"name" json:"name"` | ||||||
| 	Role      string   `db:"role" json:"role"` | 	Role      string   `db:"role" json:"role"` | ||||||
|  | 	Verified  bool     `db:"verified" json:"-"` | ||||||
| 	CreatedAt NullTime `db:"created_at" json:"createdAt"` | 	CreatedAt NullTime `db:"created_at" json:"createdAt"` | ||||||
| 	UpdatedAt NullTime `db:"updated_at" json:"updatedAt"` | 	UpdatedAt NullTime `db:"updated_at" json:"updatedAt"` | ||||||
| 	DeletedAt NullTime `db:"deleted_at" json:"deletedAt"` | 	DeletedAt NullTime `db:"deleted_at" json:"deletedAt"` | ||||||
|  | @ -126,7 +134,10 @@ func (u UserService) list(val *url.Values) (entity, *appError) { | ||||||
| 
 | 
 | ||||||
| 	users := make(Users, 0) | 	users := make(Users, 0) | ||||||
| 	sql := `SELECT id, email, 'password' AS password, name, role, | 	sql := `SELECT id, email, 'password' AS password, name, role, | ||||||
| 		created_at, updated_at, deleted_at FROM users;` | 		created_at, updated_at, deleted_at | ||||||
|  | 		FROM users | ||||||
|  | 		WHERE verified IS NOT FALSE | ||||||
|  | 		AND deleted_at IS NULL;` | ||||||
| 	if err := DBH.Select(&users, sql); err != nil { | 	if err := DBH.Select(&users, sql); err != nil { | ||||||
| 		return nil, newJSONError(err, http.StatusInternalServerError) | 		return nil, newJSONError(err, http.StatusInternalServerError) | ||||||
| 	} | 	} | ||||||
|  | @ -136,7 +147,11 @@ func (u UserService) list(val *url.Values) (entity, *appError) { | ||||||
| func (u UserService) get(id int64, genus string) (entity, *appError) { | func (u UserService) get(id int64, genus string) (entity, *appError) { | ||||||
| 	var user User | 	var user User | ||||||
| 	q := `SELECT id, email, 'password' AS password, name, role, | 	q := `SELECT id, email, 'password' AS password, name, role, | ||||||
| 		created_at, updated_at, deleted_at FROM users WHERE id=$1;` | 		created_at, updated_at, deleted_at | ||||||
|  | 		FROM users | ||||||
|  | 		WHERE id=$1 | ||||||
|  | 		AND verified IS NOT FALSE | ||||||
|  | 		AND deleted_at IS NULL;` | ||||||
| 	if err := DBH.SelectOne(&user, q, id); err != nil { | 	if err := DBH.SelectOne(&user, q, id); err != nil { | ||||||
| 		if err == sql.ErrNoRows { | 		if err == sql.ErrNoRows { | ||||||
| 			return nil, ErrUserNotFoundJSON | 			return nil, ErrUserNotFoundJSON | ||||||
|  | @ -175,10 +190,29 @@ func (u UserService) create(e *entity, claims Claims) *appError { | ||||||
| 	} | 	} | ||||||
| 	user.Password = string(hash) | 	user.Password = string(hash) | ||||||
| 	user.Role = "R" | 	user.Role = "R" | ||||||
|  | 	user.Verified = false | ||||||
| 
 | 
 | ||||||
| 	if err := DBH.Insert(user); err != nil { | 	if err := DBH.Insert(user); err != nil { | ||||||
|  | 		if err, ok := err.(*pq.Error); ok { | ||||||
|  | 			if err.Code == "23505" { | ||||||
|  | 				return ErrEmailAddressTakenJSON | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
| 		return newJSONError(err, http.StatusInternalServerError) | 		return newJSONError(err, http.StatusInternalServerError) | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
|  | 	user.Password = "password" // don't want to send the hashed PW back to the client | ||||||
|  | 
 | ||||||
|  | 	q := `INSERT INTO verification (user_id, nonce, created_at) VALUES ($1, $2, $3);` | ||||||
|  | 	nonce, err := generateNonce() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return newJSONError(err, http.StatusInternalServerError) | ||||||
|  | 	} | ||||||
|  | 	_, err = DBH.Exec(q, user.Id, nonce, ct) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return newJSONError(err, http.StatusInternalServerError) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -207,3 +241,46 @@ func dbGetUserByEmail(email string) (*User, error) { | ||||||
| 	} | 	} | ||||||
| 	return &user, nil | 	return &user, nil | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func handleUserVerify(w http.ResponseWriter, r *http.Request) { | ||||||
|  | 	nonce := mux.Vars(r)["Nonce"] | ||||||
|  | 	q := `SELECT user_id FROM verification WHERE nonce=$1;` | ||||||
|  | 
 | ||||||
|  | 	var user_id int64 | ||||||
|  | 	if err := DBH.SelectOne(&user_id, q, nonce); err != nil { | ||||||
|  | 		log.Printf("%+v", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if user_id == 0 { | ||||||
|  | 		fmt.Fprintln(w, "NOT FOUND/EXPIRED") | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	var user User | ||||||
|  | 	if err := DBH.Get(&user, user_id); err != nil { | ||||||
|  | 		fmt.Printf("%+v", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	user.UpdatedAt = currentTime() | ||||||
|  | 	user.Verified = true | ||||||
|  | 
 | ||||||
|  | 	count, err := DBH.Update(&user) | ||||||
|  | 	if err != nil { | ||||||
|  | 		fmt.Printf("%+v", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	if count != 1 { | ||||||
|  | 		fmt.Printf("%+v", "hmm") | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	q = `DELETE FROM verification WHERE user_id=$1;` | ||||||
|  | 	_, err = DBH.Exec(q, user_id) | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Printf("%+v", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	fmt.Fprintln(w, user_id) | ||||||
|  | } | ||||||
|  |  | ||||||
		Reference in a new issue
	
	 Matthew Dillon
						Matthew Dillon