From fe4b31be06a704858929d3413fcb0dace9d63eb2 Mon Sep 17 00:00:00 2001 From: Tovi Jaeschke-Rogers Date: Tue, 15 Mar 2022 21:10:20 +1030 Subject: [PATCH] Add createUser and getUsers API endpoints --- Api/JsonSerialization/DeserializeUserJson.go | 76 +++++++++++ Api/Posts.go | 2 - Api/Routes.go | 3 + Api/Users.go | 102 ++++++++++++++ Api/Users_test.go | 132 +++++++++++++++++++ Database/Init.go | 3 + Database/Users.go | 63 +++++++++ Models/Users.go | 15 +++ 8 files changed, 394 insertions(+), 2 deletions(-) create mode 100644 Api/JsonSerialization/DeserializeUserJson.go create mode 100644 Api/Users.go create mode 100644 Api/Users_test.go create mode 100644 Database/Users.go create mode 100644 Models/Users.go diff --git a/Api/JsonSerialization/DeserializeUserJson.go b/Api/JsonSerialization/DeserializeUserJson.go new file mode 100644 index 0000000..01ad7d9 --- /dev/null +++ b/Api/JsonSerialization/DeserializeUserJson.go @@ -0,0 +1,76 @@ +package JsonSerialization + +import ( + "encoding/json" + "errors" + "fmt" + "strings" + + "git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Models" + + schema "github.com/Kangaroux/go-map-schema" +) + +func DeserializeUser(data []byte, allowMissing []string, allowAllMissing bool) (Models.User, error) { + var ( + postData Models.User = Models.User{} + jsonStructureTest map[string]interface{} = make(map[string]interface{}) + jsonStructureTestResults *schema.CompareResults + field schema.FieldMissing + allowed string + missingFields []string + i int + err error + ) + + // Verify the JSON has the correct structure + json.Unmarshal(data, &jsonStructureTest) + jsonStructureTestResults, err = schema.CompareMapToStruct( + &postData, + jsonStructureTest, + &schema.CompareOpts{ + ConvertibleFunc: CanConvert, + TypeNameFunc: schema.DetailedTypeName, + }) + if err != nil { + return postData, err + } + + if len(jsonStructureTestResults.MismatchedFields) > 0 { + return postData, errors.New(fmt.Sprintf( + "MismatchedFields found when deserializing data: %s", + jsonStructureTestResults.Errors().Error(), + )) + } + + // Remove allowed missing fields from MissingFields + for _, allowed = range allowMissing { + for i, field = range jsonStructureTestResults.MissingFields { + if allowed == field.String() { + jsonStructureTestResults.MissingFields = append( + jsonStructureTestResults.MissingFields[:i], + jsonStructureTestResults.MissingFields[i+1:]..., + ) + } + } + } + + if !allowAllMissing && len(jsonStructureTestResults.MissingFields) > 0 { + for _, field = range jsonStructureTestResults.MissingFields { + missingFields = append(missingFields, field.String()) + } + + return postData, errors.New(fmt.Sprintf( + "MissingFields found when deserializing data: %s", + strings.Join(missingFields, ", "), + )) + } + + // Deserialize the JSON into the struct + err = json.Unmarshal(data, &postData) + if err != nil { + return postData, err + } + + return postData, err +} diff --git a/Api/Posts.go b/Api/Posts.go index 60f50ec..e54af45 100644 --- a/Api/Posts.go +++ b/Api/Posts.go @@ -116,8 +116,6 @@ func createPost(w http.ResponseWriter, r *http.Request) { // TODO: Add auth - log.Printf("Posts handler recieved %s request", r.Method) - requestBody, err = ioutil.ReadAll(r.Body) if err != nil { log.Printf("Error encountered reading POST body: %s\n", err.Error()) diff --git a/Api/Routes.go b/Api/Routes.go index c023cc4..4ce2674 100644 --- a/Api/Routes.go +++ b/Api/Routes.go @@ -26,6 +26,9 @@ func InitApiEndpoints() *mux.Router { router.HandleFunc("/post/{postID}/image", createPostImage).Methods("POST") router.HandleFunc("/post/{postID}/image/{imageID}", deletePostImage).Methods("DELETE") + // Define routes for users api + router.HandleFunc("/user", createUser).Methods("POST") + //router.PathPrefix("/").Handler(http.StripPrefix("/images/", http.FileServer(http.Dir("./uploads")))) return router diff --git a/Api/Users.go b/Api/Users.go new file mode 100644 index 0000000..c343b69 --- /dev/null +++ b/Api/Users.go @@ -0,0 +1,102 @@ +package Api + +import ( + "encoding/json" + "io/ioutil" + "log" + "net/http" + "net/url" + "strconv" + + "git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Api/JsonSerialization" + "git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Database" + "git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Models" +) + +func getUsers(w http.ResponseWriter, r *http.Request) { + var ( + users []Models.User + returnJson []byte + values url.Values + page, pageSize int + err error + ) + + values = r.URL.Query() + + page, err = strconv.Atoi(values.Get("page")) + if err != nil { + log.Println("Could not parse page url argument") + JsonReturn(w, 500, "An error occured") + return + } + + page, err = strconv.Atoi(values.Get("pageSize")) + if err != nil { + log.Println("Could not parse pageSize url argument") + JsonReturn(w, 500, "An error occured") + return + } + + users, err = Database.GetUsers(page, pageSize) + if err != nil { + log.Printf("An error occured: %s\n", err.Error()) + JsonReturn(w, 500, "An error occured") + return + } + + returnJson, err = json.MarshalIndent(users, "", " ") + if err != nil { + JsonReturn(w, 500, "An error occured") + return + } + + // Return updated json + w.WriteHeader(http.StatusOK) + w.Write(returnJson) +} + +func createUser(w http.ResponseWriter, r *http.Request) { + var ( + userData Models.User + requestBody []byte + err error + ) + + requestBody, err = ioutil.ReadAll(r.Body) + if err != nil { + log.Printf("Error encountered reading POST body: %s\n", err.Error()) + JsonReturn(w, 500, "An error occured") + return + } + + userData, err = JsonSerialization.DeserializeUser(requestBody, []string{ + "id", + "last_login", + }, false) + if err != nil { + log.Printf("Invalid data provided to user API: %s\n", err.Error()) + JsonReturn(w, 405, "Invalid data") + return + } + + err = Database.CheckUniqueEmail(userData.Email) + if err != nil { + JsonReturn(w, 405, "invalid_email") + return + } + + if userData.Password != userData.ConfirmPassword { + JsonReturn(w, 500, "invalid_password") + return + } + + err = Database.CreateUser(&userData) + if err != nil { + JsonReturn(w, 405, "Invalid data") + return + } + + // Return updated json + w.WriteHeader(http.StatusOK) +} diff --git a/Api/Users_test.go b/Api/Users_test.go new file mode 100644 index 0000000..8007c12 --- /dev/null +++ b/Api/Users_test.go @@ -0,0 +1,132 @@ +package Api + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "log" + "math/rand" + "net/http" + "net/http/httptest" + "os" + "path" + "runtime" + "strings" + "testing" + + "git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Database" + "git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Models" + "github.com/gorilla/mux" + "gorm.io/gorm" +) + +func init() { + // Fix working directory for tests + _, filename, _, _ := runtime.Caller(0) + dir := path.Join(path.Dir(filename), "..") + err := os.Chdir(dir) + if err != nil { + panic(err) + } + + log.SetOutput(ioutil.Discard) + Database.Init() + + r = mux.NewRouter() +} + +var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + +func RandStringRunes(n int) string { + b := make([]rune, n) + for i := range b { + b[i] = letterRunes[rand.Intn(len(letterRunes))] + } + return string(b) +} + +func Test_getUsers(t *testing.T) { + t.Log("Testing getUsers...") + + r.HandleFunc("/user", getUsers).Methods("GET") + + ts := httptest.NewServer(r) + defer ts.Close() + + var err error + for i := 0; i < 20; i++ { + userData := Models.User{ + Email: fmt.Sprintf( + "%s@email.com", + RandStringRunes(16), + ), + Password: "password", + ConfirmPassword: "password", + } + + err = Database.CreateUser(&userData) + if err != nil { + t.Errorf("Expected nil, recieved %s", err.Error()) + } + + defer Database.DB. + Session(&gorm.Session{FullSaveAssociations: true}). + Unscoped(). + Delete(&userData) + } + + res, err := http.Get(ts.URL + "/user?page=1&pageSize=10") + + if err != nil { + t.Errorf("Expected nil, recieved %s", err.Error()) + } + if res.StatusCode != http.StatusOK { + t.Errorf("Expected %d, recieved %d", http.StatusOK, res.StatusCode) + } + + getUsersData := new([]Models.User) + err = json.NewDecoder(res.Body).Decode(getUsersData) + if err != nil { + t.Errorf("Expected nil, recieved %s", err.Error()) + } + + if len(*getUsersData) != 10 { + t.Errorf("Expected 10, recieved %d", len(*getUsersData)) + } +} + +func Test_createUser(t *testing.T) { + t.Log("Testing createUser...") + + r.HandleFunc("/user", createUser).Methods("POST") + + ts := httptest.NewServer(r) + + defer ts.Close() + + postJson := ` +{ + "email": "email@email.com", + "password": "password", + "confirm_password": "password", + "first_name": "Hugh", + "last_name": "Mann" +} +` + + res, err := http.Post(ts.URL+"/user", "application/json", strings.NewReader(postJson)) + if err != nil { + t.Errorf("Expected nil, recieved %s", err.Error()) + return + } + + if res.StatusCode != http.StatusOK { + t.Errorf("Expected %d, recieved %d", http.StatusOK, res.StatusCode) + return + } + + Database.DB.Model(Models.User{}). + Select("count(*) > 0"). + Where("email = ?", "email@email.com"). + Delete(Models.User{}) +} diff --git a/Database/Init.go b/Database/Init.go index a467b64..44c1930 100644 --- a/Database/Init.go +++ b/Database/Init.go @@ -43,4 +43,7 @@ func Init() { DB.AutoMigrate(&Models.SubscriptionEmailAttachment{}) DB.AutoMigrate(&Models.SubscriptionEmail{}) DB.AutoMigrate(&Models.Subscription{}) + + log.Println("Running AutoMigrate on User tables...") + DB.AutoMigrate(&Models.User{}) } diff --git a/Database/Users.go b/Database/Users.go new file mode 100644 index 0000000..e28b898 --- /dev/null +++ b/Database/Users.go @@ -0,0 +1,63 @@ +package Database + +import ( + "errors" + + "git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Models" + + "gorm.io/gorm" +) + +func GetUsers(page, pageSize int) ([]Models.User, error) { + var ( + users []Models.User + err error + ) + + if page == 0 { + page = 1 + } + + switch { + case pageSize > 100: + pageSize = 100 + case pageSize <= 0: + pageSize = 10 + } + + err = DB.Offset(page). + Limit(pageSize). + Find(&users). + Error + + return users, err +} + +func CheckUniqueEmail(email string) error { + var ( + exists bool + err error + ) + + err = DB.Model(Models.User{}). + Select("count(*) > 0"). + Where("email = ?", email). + Find(&exists). + Error + + if err != nil { + return err + } + + if exists { + return errors.New("Invalid email") + } + + return nil +} + +func CreateUser(userData *Models.User) error { + return DB.Session(&gorm.Session{FullSaveAssociations: true}). + Create(userData). + Error +} diff --git a/Models/Users.go b/Models/Users.go new file mode 100644 index 0000000..27401e4 --- /dev/null +++ b/Models/Users.go @@ -0,0 +1,15 @@ +package Models + +import ( + "time" +) + +type User struct { + Base + Email string `gorm:"not null;unique" json:"email"` + Password string `gorm:"not null" json:"password"` + ConfirmPassword string `gorm:"-" json:"confirm_password"` + LastLogin *time.Time `json:"last_login"` + FirstName string `gorm:"not null" json:"first_name"` + LastName string `gorm:"not null" json:"last_name"` +}