parent
179a6829a3
commit
fe0cdbd3cb
@ -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
|
||||
|
||||
import (
|
||||
"github.com/dustinpianalto/quartermaster/internal/utils"
|
||||
"github.com/dustinpianalto/quartermaster/pkg/api/users"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func GetRouter() *mux.Router {
|
||||
router := mux.NewRouter().StrictSlash(true)
|
||||
router.HandleFunc("/", homePage).Methods("GET")
|
||||
router.HandleFunc("/", healthcheck).Methods("GET")
|
||||
router.HandleFunc("/healthcheck", healthcheck).Methods("GET")
|
||||
utils.Mount(router, "/users", users.GetRouter())
|
||||
return router
|
||||
}
|
||||
|
||||
@ -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,
|
||||
})
|
||||
}
|
||||
@ -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…
Reference in new issue