|
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"fmt"
|
|
|
|
|
"net/http"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"crypto/rand"
|
|
|
|
|
"reflect"
|
|
|
|
|
"log"
|
|
|
|
|
"os"
|
|
|
|
|
|
|
|
|
|
// muxer and form parser
|
|
|
|
|
"github.com/gorilla/mux"
|
|
|
|
|
"github.com/gorilla/handlers"
|
|
|
|
|
"github.com/gorilla/schema"
|
|
|
|
|
|
|
|
|
|
// error returns for HandlerFunc
|
|
|
|
|
"github.com/creack/ehttp"
|
|
|
|
|
|
|
|
|
|
// JWT stuff
|
|
|
|
|
"github.com/auth0/go-jwt-middleware"
|
|
|
|
|
"github.com/dgrijalva/jwt-go"
|
|
|
|
|
|
|
|
|
|
// form validation
|
|
|
|
|
"github.com/asaskevich/govalidator"
|
|
|
|
|
|
|
|
|
|
// database backend
|
|
|
|
|
"github.com/jinzhu/gorm"
|
|
|
|
|
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
|
|
|
|
|
|
|
|
|
// password hashing
|
|
|
|
|
"golang.org/x/crypto/scrypt"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type User struct {
|
|
|
|
|
gorm.Model
|
|
|
|
|
|
|
|
|
|
Name string `valid:"alphanum,required" sql:"unique"`
|
|
|
|
|
Pass string `valid:"runelength(8|999),required"`
|
|
|
|
|
Salt string `valid:"optional"`
|
|
|
|
|
Email string `valid:"email,required" sql:"unique"`
|
|
|
|
|
Lists []List
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type List struct {
|
|
|
|
|
gorm.Model
|
|
|
|
|
UserID uint `gorm:"unique_index:idx_userid_name"`
|
|
|
|
|
|
|
|
|
|
Name string `valid:"alphanum,required" gorm:"unique_index:idx_userid_name"`
|
|
|
|
|
Icon string `valid:"alphanum,optional"`
|
|
|
|
|
Items []Item
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type Item struct {
|
|
|
|
|
gorm.Model
|
|
|
|
|
ListID uint
|
|
|
|
|
|
|
|
|
|
Name string `valid:"alphanum,required"`
|
|
|
|
|
Amount float64 `valid:"optional"`
|
|
|
|
|
Bought bool `valid:"required"`
|
|
|
|
|
Unit Unit
|
|
|
|
|
UnitID uint
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type Unit struct {
|
|
|
|
|
gorm.Model
|
|
|
|
|
|
|
|
|
|
Name string `valid:"alphanum,required"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var db *gorm.DB
|
|
|
|
|
|
|
|
|
|
func getToken(w http.ResponseWriter, r *http.Request) error {
|
|
|
|
|
var userInput User
|
|
|
|
|
var userRecord User
|
|
|
|
|
var err error
|
|
|
|
|
decoder := schema.NewDecoder()
|
|
|
|
|
|
|
|
|
|
// Parse form data
|
|
|
|
|
err = r.ParseForm()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return ehttp.NewErrorf(http.StatusInternalServerError, "could not parse form", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Decode to struct
|
|
|
|
|
err = decoder.Decode(&userInput, r.Form)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return ehttp.NewErrorf(http.StatusInternalServerError, "could not decode user from form", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Find user in DB
|
|
|
|
|
db.First(&userRecord)
|
|
|
|
|
if db.Error != nil {
|
|
|
|
|
return ehttp.NewErrorf(http.StatusInternalServerError, "database lookup error", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check hashed PW
|
|
|
|
|
hash, err := scrypt.Key([]byte(userInput.Pass), []byte(userRecord.Salt), 16384, 8, 1, 32)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return ehttp.NewErrorf(http.StatusInternalServerError, "cannot hash pass", err)
|
|
|
|
|
}
|
|
|
|
|
if userRecord.Name == userInput.Name && reflect.DeepEqual(hash, []byte(userRecord.Pass)) {
|
|
|
|
|
// Generate JWT-token
|
|
|
|
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
|
|
|
|
"name":userRecord.Name,
|
|
|
|
|
})
|
|
|
|
|
tokenString, err := token.SignedString([]byte("TODO"))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return ehttp.NewErrorf(http.StatusForbidden, "could not construct token", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Reply with token
|
|
|
|
|
jsonOut, _ := json.Marshal(map[string]string{"token": tokenString})
|
|
|
|
|
fmt.Fprint(w, string(jsonOut))
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// User was not found
|
|
|
|
|
return ehttp.NewErrorf(http.StatusForbidden, "Could not find user/pass")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func register(w http.ResponseWriter, r *http.Request) error {
|
|
|
|
|
decoder := schema.NewDecoder()
|
|
|
|
|
|
|
|
|
|
// Parse http request to request-struct
|
|
|
|
|
err := r.ParseForm()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return ehttp.NewErrorf(http.StatusInternalServerError, "could not parse form", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Decode request into struct
|
|
|
|
|
var input User
|
|
|
|
|
err = decoder.Decode(&input, r.PostForm)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return ehttp.NewErrorf(http.StatusInternalServerError, "could not decode user", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Validate struct
|
|
|
|
|
res, err := govalidator.ValidateStruct(input)
|
|
|
|
|
if err != nil || res != true {
|
|
|
|
|
return ehttp.NewErrorf(http.StatusBadRequest, "could not validate your data", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if users already exists
|
|
|
|
|
/*countName := 0; countEmail := 0
|
|
|
|
|
db.Model(&User{}).Where("name = ?", input.Name).Count(&countName)
|
|
|
|
|
db.Model(&User{}).Where("email = ?", input.Email).Count(&countEmail)
|
|
|
|
|
if countName != 0 || countEmail != 0{
|
|
|
|
|
return ehttp.NewErrorf(http.StatusConflict, "username or email already exists")
|
|
|
|
|
}*/
|
|
|
|
|
|
|
|
|
|
// Generate salt
|
|
|
|
|
salt := make([]byte, 64)
|
|
|
|
|
_, err = rand.Read(salt)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return ehttp.NewErrorf(http.StatusInternalServerError, "could not generate salt", err)
|
|
|
|
|
}
|
|
|
|
|
input.Salt = string(salt)
|
|
|
|
|
|
|
|
|
|
// Generate scrypt hash of pass
|
|
|
|
|
hash, err := scrypt.Key([]byte(input.Pass), salt, 16384, 8, 1, 32)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return ehttp.NewErrorf(http.StatusInternalServerError, "could not hash pass", err)
|
|
|
|
|
}
|
|
|
|
|
input.Pass = string(hash)
|
|
|
|
|
|
|
|
|
|
// add to database
|
|
|
|
|
db.NewRecord(input)
|
|
|
|
|
retCreate := db.Create(&input)
|
|
|
|
|
if retCreate.Error != nil {
|
|
|
|
|
return ehttp.NewErrorf(http.StatusInternalServerError, "could not create user", err)
|
|
|
|
|
}
|
|
|
|
|
log.Println("registered user", input.Name)
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func createList(w http.ResponseWriter, r *http.Request) error {
|
|
|
|
|
decoder := schema.NewDecoder()
|
|
|
|
|
decoder.IgnoreUnknownKeys(true)
|
|
|
|
|
|
|
|
|
|
// Parse http request to request-struct
|
|
|
|
|
err := r.ParseForm()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return ehttp.NewErrorf(http.StatusInternalServerError, "could not parse form", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Decode request into struct
|
|
|
|
|
var input List
|
|
|
|
|
err = decoder.Decode(&input, r.PostForm)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return ehttp.NewErrorf(http.StatusInternalServerError, "could not decode ", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Validate struct
|
|
|
|
|
res, err := govalidator.ValidateStruct(input)
|
|
|
|
|
if err != nil || res != true {
|
|
|
|
|
return ehttp.NewErrorf(http.StatusBadRequest, "could not validate your data", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Find request-corresponding user
|
|
|
|
|
// TODO: push into middleware?
|
|
|
|
|
userName := r.Context().Value("user").(*jwt.Token).Claims.(jwt.MapClaims)["name"].(string)
|
|
|
|
|
user := User{Name: userName}
|
|
|
|
|
db.Where(&user).First(&user)
|
|
|
|
|
if user.ID == 0 { // TODO: ugly way to check for user-not-found
|
|
|
|
|
return ehttp.NewErrorf(http.StatusForbidden, "cannot find claimed user")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// create the new list
|
|
|
|
|
db.NewRecord(&input)
|
|
|
|
|
ret := db.Create(&input)
|
|
|
|
|
if ret.Error != nil {
|
|
|
|
|
return ehttp.NewErrorf(http.StatusInternalServerError, "cannot create list in db")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
|
var err error
|
|
|
|
|
|
|
|
|
|
// TODO: maybe move to init()?
|
|
|
|
|
db, err = gorm.Open("sqlite3", "test.db")
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Fatalln("cannot open db", err)
|
|
|
|
|
}
|
|
|
|
|
db.LogMode(false)
|
|
|
|
|
|
|
|
|
|
jwt := jwtmiddleware.New(jwtmiddleware.Options{
|
|
|
|
|
Extractor: func(r *http.Request) (string,error) {
|
|
|
|
|
r.ParseForm()
|
|
|
|
|
return r.Form["token"][0], nil
|
|
|
|
|
},
|
|
|
|
|
ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) {
|
|
|
|
|
return []byte("TODO"), nil
|
|
|
|
|
}})
|
|
|
|
|
|
|
|
|
|
// Create or update schemas …
|
|
|
|
|
db.AutoMigrate(&User{})
|
|
|
|
|
db.AutoMigrate(&List{})
|
|
|
|
|
db.AutoMigrate(&Item{})
|
|
|
|
|
db.AutoMigrate(&Unit{})
|
|
|
|
|
db.Model(&User{}).Related(&List{})
|
|
|
|
|
db.Model(&List{}).Related(&Item{})
|
|
|
|
|
db.Model(&Item{}).Related(&Unit{})
|
|
|
|
|
defer db.Close()
|
|
|
|
|
|
|
|
|
|
// define URL handlers
|
|
|
|
|
r := mux.NewRouter()
|
|
|
|
|
|
|
|
|
|
// User Management
|
|
|
|
|
r.Handle("/users", ehttp.HandlerFunc(register)).Methods("POST") // Make new user
|
|
|
|
|
r.Handle("/users", ehttp.HandlerFunc(getToken)).Methods("GET") // Get user „info“(=token)
|
|
|
|
|
// TODO: delete, modify
|
|
|
|
|
|
|
|
|
|
// List Management
|
|
|
|
|
r.Handle("/lists", jwt.Handler(ehttp.HandlerFunc(createList))).Methods("POST") // Make a new list
|
|
|
|
|
r.Handle("/lists", jwt.Handler(ehttp.HandlerFunc(nil))).Methods("GET") // Get list of lists
|
|
|
|
|
r.Handle("/lists/{name}", jwt.Handler(ehttp.HandlerFunc(nil))).Methods("PUT") // Update list (name, icon, …)
|
|
|
|
|
r.Handle("/lists/{name}", jwt.Handler(ehttp.HandlerFunc(nil))).Methods("DELETE") // Delete a list
|
|
|
|
|
r.Handle("/lists/{name}", jwt.Handler(ehttp.HandlerFunc(nil))).Methods("GET") // Get list content (i.e., items)
|
|
|
|
|
// List sharing
|
|
|
|
|
r.Handle("/lists/{name}/sharers", jwt.Handler(ehttp.HandlerFunc(nil))).Methods("POST") // Add new sharer
|
|
|
|
|
r.Handle("/lists/{name}/sharers", jwt.Handler(ehttp.HandlerFunc(nil))).Methods("GET") // Get list of sharers
|
|
|
|
|
r.Handle("/lists/{name}/sharers/{user}", jwt.Handler(ehttp.HandlerFunc(nil))).Methods("DELETE") // Remove sharer
|
|
|
|
|
|
|
|
|
|
r.Handle("/lists/{name}/items", jwt.Handler(ehttp.HandlerFunc(nil))).Methods("PUT") // Add new item
|
|
|
|
|
r.Handle("/lists/{name}/items/{item}", jwt.Handler(ehttp.HandlerFunc(nil))).Methods("DELETE") // Delete item
|
|
|
|
|
r.Handle("/lists/{name}/items/{item}", jwt.Handler(ehttp.HandlerFunc(nil))).Methods("POST") // Update item
|
|
|
|
|
|
|
|
|
|
// Unit-history (unit is a ever-growing list only used for auto-completion)
|
|
|
|
|
r.Handle("/units", jwt.Handler(ehttp.HandlerFunc(nil))).Methods("POST") // Make a new unit
|
|
|
|
|
r.Handle("/units", jwt.Handler(ehttp.HandlerFunc(nil))).Methods("GET") // Get list of units
|
|
|
|
|
|
|
|
|
|
loggedRouter := handlers.LoggingHandler(os.Stdout, r)
|
|
|
|
|
http.Handle("/", loggedRouter)
|
|
|
|
|
http.ListenAndServe(":8000", nil)
|
|
|
|
|
}
|
|
|
|
|
|