1
0
Fork 0
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

281 lines
8.1 KiB

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
8 years ago
"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
8 years ago
Name string `valid:"alphanum,required" sql:"unique"`
Pass string `valid:"runelength(8|999),required"`
Salt string `valid:"optional"`
8 years ago
Email string `valid:"email,required" sql:"unique"`
Lists []List
}
type List struct {
gorm.Model
8 years ago
UserID uint `gorm:"unique_index:idx_userid_name"`
8 years ago
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
8 years ago
/*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")
8 years ago
}*/
// 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
8 years ago
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
}
8 years ago
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)
8 years ago
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)
}