Add user management routes

main
DustyP 4 years ago
parent 179a6829a3
commit fe0cdbd3cb

@ -14,7 +14,7 @@ import (
) )
func main() { func main() {
var migrationsVersion uint = 2 // Update when there is a new migration var migrationsVersion uint = 3 // Update when there is a new migration
postgres.ConnectDatabase(os.Getenv("DATABASE_URL"), migrationsVersion) postgres.ConnectDatabase(os.Getenv("DATABASE_URL"), migrationsVersion)
services.InitServices() services.InitServices()

@ -4,6 +4,7 @@ go 1.17
require ( require (
github.com/gobuffalo/here v0.6.0 // indirect github.com/gobuffalo/here v0.6.0 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang-migrate/migrate/v4 v4.15.1 // indirect github.com/golang-migrate/migrate/v4 v4.15.1 // indirect
github.com/gorilla/mux v1.8.0 // indirect github.com/gorilla/mux v1.8.0 // indirect
github.com/hashicorp/errwrap v1.0.0 // indirect github.com/hashicorp/errwrap v1.0.0 // indirect
@ -11,4 +12,5 @@ require (
github.com/lib/pq v1.10.4 // indirect github.com/lib/pq v1.10.4 // indirect
github.com/markbates/pkger v0.17.1 // indirect github.com/markbates/pkger v0.17.1 // indirect
go.uber.org/atomic v1.6.0 // indirect go.uber.org/atomic v1.6.0 // indirect
golang.org/x/crypto v0.0.0-20211115234514-b4de73f9ece8 // indirect
) )

@ -397,6 +397,8 @@ github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5
github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-migrate/migrate v3.5.4+incompatible h1:R7OzwvCJTCgwapPCiX6DyBiu2czIUMDCB118gFTKTUA= github.com/golang-migrate/migrate v3.5.4+incompatible h1:R7OzwvCJTCgwapPCiX6DyBiu2czIUMDCB118gFTKTUA=
github.com/golang-migrate/migrate v3.5.4+incompatible/go.mod h1:IsVUlFN5puWOmXrqjgGUfIRIbU7mr8oNBE2tyERd9Wk= github.com/golang-migrate/migrate v3.5.4+incompatible/go.mod h1:IsVUlFN5puWOmXrqjgGUfIRIbU7mr8oNBE2tyERd9Wk=
github.com/golang-migrate/migrate/v4 v4.15.1 h1:Sakl3Nm6+wQKq0Q62tpFMi5a503bgGhceo2icrgQ9vM= github.com/golang-migrate/migrate/v4 v4.15.1 h1:Sakl3Nm6+wQKq0Q62tpFMi5a503bgGhceo2icrgQ9vM=
@ -907,6 +909,8 @@ golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211115234514-b4de73f9ece8 h1:5QRxNnVsaJP6NAse0UdkRgL3zHMvCRRkrDVLNdNpdy4=
golang.org/x/crypto v0.0.0-20211115234514-b4de73f9ece8/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=

@ -21,6 +21,7 @@ var (
VitaminService quartermaster.VitaminService VitaminService quartermaster.VitaminService
GroupService quartermaster.GroupService GroupService quartermaster.GroupService
CategoryService quartermaster.CategoryService CategoryService quartermaster.CategoryService
UserService quartermaster.UserService
) )
func ConnectDatabase(dbConnString string, version uint) { func ConnectDatabase(dbConnString string, version uint) {
@ -66,4 +67,5 @@ func initServices(db *sql.DB) {
VitaminService = vitaminService{db: db} VitaminService = vitaminService{db: db}
GroupService = groupService{db: db} GroupService = groupService{db: db}
CategoryService = categoryService{db: db} CategoryService = categoryService{db: db}
UserService = userService{db: db}
} }

@ -0,0 +1,10 @@
BEGIN;
ALTER TABLE items DROP COLUMN owner_id ;
ALTER TABLE locations DROP COLUMN owner_id;
ALTER TABLE groups DROP COLUMN owner_id;
ALTER TABLE categories DROP COLUMN owner_id;
DROP TABLE IF EXISTS users;
COMMIT;

@ -0,0 +1,16 @@
BEGIN;
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY ,
username VARCHAR(255),
password VARCHAR(60),
CONSTRAINT uniq_username
UNIQUE (username)
);
ALTER TABLE items ADD COLUMN owner_id INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE;
ALTER TABLE locations ADD COLUMN owner_id INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE;
ALTER TABLE groups ADD COLUMN owner_id INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE;
ALTER TABLE categories ADD COLUMN owner_id INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE;
COMMIT;

@ -0,0 +1,37 @@
package postgres
import (
"database/sql"
"github.com/dustinpianalto/quartermaster"
)
type userService struct {
db *sql.DB
}
func (s userService) User(username string) (*quartermaster.User, error) {
var user quartermaster.User
queryString := "SELECT id, username, password FROM users WHERE username = $1"
row := s.db.QueryRow(queryString, username)
err := row.Scan(&user.ID, &user.Username, &user.Password)
return &user, err
}
func (s userService) AddUser(u *quartermaster.User) (*quartermaster.User, error) {
queryString := "INSERT INTO users (username, password) VALUES ($1, $2) RETURNING id"
err := s.db.QueryRow(queryString, u.Username, u.Password).Scan(&u.ID)
return u, err
}
func (s userService) RemoveUser(u *quartermaster.User) error {
queryString := "DELETE FROM users WHERE id = $1"
_, err := s.db.Exec(queryString, u.ID)
return err
}
func (s userService) UpdateUser(u *quartermaster.User) error {
queryString := "UPDATE users SET password = $1 WHERE id = $2"
_, err := s.db.Exec(queryString, u.Password, u.ID)
return err
}

@ -1,12 +1,15 @@
package api package api
import ( import (
"github.com/dustinpianalto/quartermaster/internal/utils"
"github.com/dustinpianalto/quartermaster/pkg/api/users"
"github.com/gorilla/mux" "github.com/gorilla/mux"
) )
func GetRouter() *mux.Router { func GetRouter() *mux.Router {
router := mux.NewRouter().StrictSlash(true) router := mux.NewRouter().StrictSlash(true)
router.HandleFunc("/", homePage).Methods("GET") router.HandleFunc("/", healthcheck).Methods("GET")
router.HandleFunc("/healthcheck", healthcheck).Methods("GET") router.HandleFunc("/healthcheck", healthcheck).Methods("GET")
utils.Mount(router, "/users", users.GetRouter())
return router return router
} }

@ -6,13 +6,6 @@ import (
"net/http" "net/http"
) )
func homePage(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, "{\"status\": \"success\"}")
log.Println("{\"endpoint\": \"/\", \"status\": \"success\"}")
}
func healthcheck(w http.ResponseWriter, r *http.Request) { func healthcheck(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")

@ -0,0 +1,11 @@
package users
import "github.com/gorilla/mux"
func GetRouter() *mux.Router {
router := mux.NewRouter().StrictSlash(true)
router.HandleFunc("/login", login).Methods("POST")
router.HandleFunc("/register", register).Methods("POST")
router.HandleFunc("/refresh", refresh).Methods("POST")
return router
}

@ -0,0 +1,143 @@
package users
import (
"encoding/json"
"log"
"net/http"
"os"
"time"
"github.com/dustinpianalto/quartermaster"
"github.com/dustinpianalto/quartermaster/pkg/services"
"github.com/golang-jwt/jwt"
"golang.org/x/crypto/bcrypt"
)
var jwtKey = []byte(os.Getenv("JWT_KEY"))
func login(w http.ResponseWriter, r *http.Request) {
var userReq quartermaster.User
err := json.NewDecoder(r.Body).Decode(&userReq)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
log.Println(err)
return
}
user, err := services.UserService.User(userReq.Username)
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
log.Println(err)
return
}
if bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(userReq.Password)) != nil {
w.WriteHeader(http.StatusUnauthorized)
log.Println(err)
return
}
expires := time.Now().Add(10 * time.Minute)
claims := &quartermaster.Claims{
ID: user.ID,
Username: user.Username,
StandardClaims: jwt.StandardClaims{
ExpiresAt: expires.Unix(),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS512, claims)
tokenString, err := token.SignedString(jwtKey)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
log.Println(err)
return
}
http.SetCookie(w, &http.Cookie{
Name: "token",
Value: tokenString,
Expires: expires,
})
http.Redirect(w, r, "/", http.StatusFound) // Redirect with 302
}
func register(w http.ResponseWriter, r *http.Request) {
var user *quartermaster.User
err := json.NewDecoder(r.Body).Decode(&user)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
p, err := bcrypt.GenerateFromPassword([]byte(user.Password), 5)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
user.Password = string(p)
user, err = services.UserService.AddUser(user)
if err != nil {
w.WriteHeader(http.StatusConflict)
log.Println(err)
return
}
http.Redirect(w, r, "/login", http.StatusFound)
}
func refresh(w http.ResponseWriter, r *http.Request) {
c, err := r.Cookie("token")
if err != nil {
if err == http.ErrNoCookie {
w.WriteHeader(http.StatusUnauthorized)
return
}
w.WriteHeader(http.StatusBadRequest)
return
}
tknStr := c.Value
claims := &quartermaster.Claims{}
tkn, err := jwt.ParseWithClaims(tknStr, claims, func(token *jwt.Token) (interface{}, error) {
return jwtKey, nil
})
if err != nil {
if err == jwt.ErrSignatureInvalid {
w.WriteHeader(http.StatusUnauthorized)
return
}
w.WriteHeader(http.StatusBadRequest)
return
}
if !tkn.Valid {
w.WriteHeader(http.StatusUnauthorized)
return
}
// We ensure that a new token is not issued until enough time has elapsed
// In this case, a new token will only be issued if the old token is within
// 2 minutes of expiry. Otherwise, return a bad request status
if time.Until(time.Unix(claims.StandardClaims.ExpiresAt, 0)) >= 2*time.Minute {
w.WriteHeader(http.StatusBadRequest)
return
}
// Now, create a new token for the current use, with a renewed expiration time
expirationTime := time.Now().Add(10 * time.Minute)
claims.StandardClaims.ExpiresAt = expirationTime.Unix()
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString(jwtKey)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
// Set the new token as the users `token` cookie
http.SetCookie(w, &http.Cookie{
Name: "token",
Value: tokenString,
Expires: expirationTime,
})
}

@ -14,6 +14,7 @@ var (
VitaminService quartermaster.VitaminService VitaminService quartermaster.VitaminService
GroupService quartermaster.GroupService GroupService quartermaster.GroupService
CategoryService quartermaster.CategoryService CategoryService quartermaster.CategoryService
UserService quartermaster.UserService
) )
func InitServices() { func InitServices() {
@ -23,5 +24,6 @@ func InitServices() {
VitaminService = postgres.VitaminService VitaminService = postgres.VitaminService
GroupService = postgres.GroupService GroupService = postgres.GroupService
CategoryService = postgres.CategoryService CategoryService = postgres.CategoryService
UserService = postgres.UserService
log.Println("Services Initialized") log.Println("Services Initialized")
} }

@ -0,0 +1,22 @@
package quartermaster
import "github.com/golang-jwt/jwt"
type User struct {
ID int `json:"id"`
Username string `json:"username"`
Password string `json:"password"`
}
type UserService interface {
User(string) (*User, error)
AddUser(*User) (*User, error)
RemoveUser(*User) error
UpdateUser(*User) error
}
type Claims struct {
ID int `json:"id"`
Username string `json:"username"`
jwt.StandardClaims
}
Loading…
Cancel
Save