diff --git a/README.md b/README.md index b260148f..78c347a4 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ This `Admin` JWT should be passed as the `Authorization` header when making a re There are a couple other useful but not necessary tools for working on the API. The first is a GUI tool for viewing and modifying the database. There are many options including [MongoDB Compass](https://www.mongodb.com/products/compass) and [Robo 3T](https://robomongo.org/). You will also want to install [Postman](https://www.getpostman.com/) for making requests to the API when testing. ## Building, Testing, and Running the API -In order to simply API development `make` is used for building, testing, and running the API. All `make` commands can be run from the root of the repository and they will properly find and operate on all of the services. +In order to simplify API development `make` is used for building, testing, and running the API. All `make` commands can be run from the root of the repository and they will properly find and operate on all of the services. ### Building the API Run the following command from the root of the repository. The gateway and all services will be built into `bin`. diff --git a/config/dev_config.json b/config/dev_config.json index a0bbb7a6..914a1236 100644 --- a/config/dev_config.json +++ b/config/dev_config.json @@ -11,6 +11,7 @@ "STAT_SERVICE": "http://localhost:8011", "NOTIFICATIONS_SERVICE": "http://localhost:8012", "PROJECT_SERVICE": "http://localhost:8013", + "RECOGNITION_SERVICE": "http://localhost:8014", "GATEWAY_PORT": "8000", "AUTH_PORT": ":8002", @@ -25,6 +26,8 @@ "STAT_PORT": ":8011", "NOTIFICATIONS_PORT": ":8012", "PROJECT_PORT": ":8013", + "RECOGNITION_PORT": ":8014", + "AUTH_DB_HOST": "localhost", "USER_DB_HOST": "localhost", @@ -38,6 +41,8 @@ "STAT_DB_HOST": "localhost", "NOTIFICATIONS_DB_HOST": "localhost", "PROJECT_DB_HOST": "localhost", + "RECOGNITION_DB_HOST": "localhost", + "AUTH_DB_NAME": "auth", "USER_DB_NAME": "user", @@ -51,6 +56,7 @@ "STAT_DB_NAME": "stat", "NOTIFICATIONS_DB_NAME": "notifications", "PROJECT_DB_NAME": "project", + "RECOGNITION_DB_NAME": "recognition", "S3_REGION": "us-east-1", "S3_BUCKET": "hackillinois-upload-2019", diff --git a/config/test_config.json b/config/test_config.json index 511f7936..ca001481 100644 --- a/config/test_config.json +++ b/config/test_config.json @@ -11,6 +11,7 @@ "STAT_SERVICE": "http://localhost:8011", "NOTIFICATIONS_SERVICE": "http://localhost:8012", "PROJECT_SERVICE": "http://localhost:8013", + "RECOGNITION_SERVICE": "http://localhost:8014", "GATEWAY_PORT": "8000", "AUTH_PORT": ":8002", @@ -25,6 +26,7 @@ "STAT_PORT": ":8011", "NOTIFICATIONS_PORT": ":8012", "PROJECT_PORT": ":8013", + "RECOGNITION_PORT": ":8014", "AUTH_DB_HOST": "localhost", "USER_DB_HOST": "localhost", @@ -38,6 +40,7 @@ "STAT_DB_HOST": "localhost", "NOTIFICATIONS_DB_HOST": "localhost", "PROJECT_DB_HOST": "localhost", + "RECOGNITION_DB_HOST": "localhost", "AUTH_DB_NAME": "test-auth", "USER_DB_NAME": "test-user", @@ -51,6 +54,7 @@ "STAT_DB_NAME": "test-stat", "NOTIFICATIONS_DB_NAME": "test-notifications", "PROJECT_DB_NAME": "test-project", + "RECOGNITION_DB_NAME": "recognition", "S3_REGION": "us-east-1", "S3_BUCKET": "hackillinois-upload-2019", diff --git a/container/env.template b/container/env.template index d51ba5fd..e21cce05 100644 --- a/container/env.template +++ b/container/env.template @@ -21,6 +21,7 @@ MAIL_DB_HOST= EVENT_DB_HOST= STAT_DB_HOST= NOTIFICATIONS_DB_HOST= +RECOGNITION_DB_HOST= # Set the oauth client id and secret for your GitHub, Google, and Linkedin applications GITHUB_CLIENT_ID= diff --git a/documentation/docs/reference/services/Recognition.md b/documentation/docs/reference/services/Recognition.md new file mode 100644 index 00000000..0be6aeb9 --- /dev/null +++ b/documentation/docs/reference/services/Recognition.md @@ -0,0 +1,139 @@ +Recognition +===== + + +GET /recognition/ +--------------------- + +Returns a list of all recognitions. + +Response format: +``` +{ + "recognitions": [ + { + "id": "81855ad8681d0d86d1e91e00167939cb", + "name": "Example Recognition 10", + "description": "This is a description", + "presenter": "Example presenter", + "recognitionId": "81855ad8681d0d86d1e91e00167939cb", + "recipients": [ + { + "type": "PROJECT", + "typeId": "52fdfc072182654f163f5f0f9a621d72" + } + ], + "tags": [ + "Data Science", + "Mobile" + ] + }, + { + "id": "9566c74d10037c4d7bbb0407d1e2c649", + "name": "Best Computer Security", + "description": "Good mastery of safe coding practices", + "presenter": "HackIllinois", + "recognitionId": "52fdfc072182654f163f5f0f9a621d72", + "recipients": [ + { + "type": "INDIVIDUAL", + "typeId": "github09829234" + } + ], + "tags": [ + "Security", + "Virus" + ] + } + ] +} +``` + +POST /recognition/ +----------- + +Creates an recognition with the requested fields. Returns the created recognition. + +Request format: +``` +{ + "name": "Example Recognition 10", + "description": "This is a description", + "presenter": "Example presenter", + "recognitionId": "81855ad8681d0d86d1e91e00167939cb", + "tags": ["Data Science", "Mobile"], + "recipients": [ + { + "type": "ALL" + } + ] +} +``` + +Response format: +``` +{ + "name": "Example Recognition 10", + "description": "This is a description", + "presenter": "Example presenter", + "recognitionId": "81855ad8681d0d86d1e91e00167939cb", + "tags": ["Data Science", "Mobile"], + "recipients": [ + { + "type": "ALL" + } + ] +} +``` + +DELETE /recognition/RECOGNITIONID/ +----------- + +Endpoint to delete an recognition with name `RECOGNITIONID` + +Response format: +``` +{ + "name": "Example Recognition 10", + "description": "This is a description", + "presenter": "Example presenter", + "recognitionId": "81855ad8681d0d86d1e91e00167939cb", + "tags": ["Data Science", "Mobile"], + "recipients": [ + { + "type": "ALL" + } + ] +} +``` + + +GET /recognition/filter/?key=value +--------------------- + +Returns all recognitions, filtered with the given key-value pairs. + +Response format: +``` +{ + "recognitions": [ + { + "id": "81855ad8681d0d86d1e91e00167939cb", + "name": "Example Recognition 10", + "description": "This is a description", + "presenter": "Example presenter", + "recognitionId": "81855ad8681d0d86d1e91e00167939cb", + "recipients": [ + { + "type": "PROJECT", + "typeId": "52fdfc072182654f163f5f0f9a621d72" + } + ], + "tags": [ + "Data Science", + "Mobile" + ] + } + ] +} +``` diff --git a/gateway/config/config.go b/gateway/config/config.go index 50ed41fd..4cc98bf4 100644 --- a/gateway/config/config.go +++ b/gateway/config/config.go @@ -24,6 +24,8 @@ var EVENT_SERVICE string var STAT_SERVICE string var NOTIFICATIONS_SERVICE string var PROJECT_SERVICE string +var RECOGNITION_SERVICE string + func Initialize() error { @@ -111,6 +113,12 @@ func Initialize() error { return err } + RECOGNITION_SERVICE, err = cfg_loader.Get("RECOGNITION_SERVICE") + + if err != nil { + return err + } + port_str, err := cfg_loader.Get("GATEWAY_PORT") if err != nil { diff --git a/gateway/services/recognition.go b/gateway/services/recognition.go new file mode 100644 index 00000000..acfbde02 --- /dev/null +++ b/gateway/services/recognition.go @@ -0,0 +1,36 @@ +package services + +import ( + "net/http" + + "github.com/HackIllinois/api/gateway/config" + "github.com/HackIllinois/api/gateway/middleware" + "github.com/HackIllinois/api/gateway/models" + "github.com/arbor-dev/arbor" + "github.com/justinas/alice" +) + +const RecognitionFormat string = "JSON" + +var RecognitionRoutes = arbor.RouteCollection{ + arbor.Route{ + "GetAllRecognitions", + "GET", + "/recognition/", + alice.New(middleware.IdentificationMiddleware).ThenFunc(GetRecognition).ServeHTTP, + }, + arbor.Route{ + "CreateRecognition", + "POST", + "/recognition/", + alice.New(middleware.AuthMiddleware([]models.Role{models.AdminRole}), middleware.IdentificationMiddleware).ThenFunc(CreateRecognition).ServeHTTP, + }, +} + +func GetRecognition(w http.ResponseWriter, r *http.Request) { + arbor.GET(w, config.RECOGNITION_SERVICE+r.URL.String(), RecognitionFormat, "", r) +} + +func CreateRecognition(w http.ResponseWriter, r *http.Request) { + arbor.POST(w, config.RECOGNITION_SERVICE+r.URL.String(), RecognitionFormat, "", r) +} diff --git a/gateway/services/services.go b/gateway/services/services.go index 36e44eea..5aac715c 100644 --- a/gateway/services/services.go +++ b/gateway/services/services.go @@ -24,6 +24,7 @@ func Initialize() error { "stat": config.STAT_SERVICE, "notifications": config.NOTIFICATIONS_SERVICE, "project": config.PROJECT_SERVICE, + "recognition": config.RECOGNITION_SERVICE, } return nil @@ -65,6 +66,7 @@ func RegisterAPIs() arbor.RouteCollection { Routes = append(Routes, StatRoutes...) Routes = append(Routes, NotificationsRoutes...) Routes = append(Routes, ProjectRoutes...) + Routes = append(Routes, RecognitionRoutes...) Routes = append(Routes, HealthRoutes...) Routes = append(Routes, ReloadRoutes...) return Routes diff --git a/log/access.log b/log/access.log deleted file mode 100644 index e69de29b..00000000 diff --git a/main.go b/main.go index 804160e4..56b754d2 100644 --- a/main.go +++ b/main.go @@ -18,6 +18,7 @@ import ( "github.com/HackIllinois/api/services/stat" "github.com/HackIllinois/api/services/upload" "github.com/HackIllinois/api/services/user" + "github.com/HackIllinois/api/services/recognition" ) var SERVICE_ENTRYPOINTS = map[string](func()){ @@ -34,6 +35,7 @@ var SERVICE_ENTRYPOINTS = map[string](func()){ "stat": stat.Entry, "notifications": notifications.Entry, "project": project.Entry, + "recognition": recognition.Entry, } func StartAll() { diff --git a/scripts/run.sh b/scripts/run.sh index 24e951ba..7efb4d19 100755 --- a/scripts/run.sh +++ b/scripts/run.sh @@ -28,6 +28,7 @@ $REPO_ROOT/bin/hackillinois-api --service event & $REPO_ROOT/bin/hackillinois-api --service stat & $REPO_ROOT/bin/hackillinois-api --service notifications & $REPO_ROOT/bin/hackillinois-api --service project & +$REPO_ROOT/bin/hackillinois-api --service recognition & $REPO_ROOT/bin/hackillinois-api --service gateway & diff --git a/services/recognition/config/config.go b/services/recognition/config/config.go new file mode 100644 index 00000000..360710e4 --- /dev/null +++ b/services/recognition/config/config.go @@ -0,0 +1,41 @@ +package config + +import ( + "os" + + "github.com/HackIllinois/api/common/configloader" +) + +var RECOGNITION_DB_HOST string +var RECOGNITION_DB_NAME string + +var RECOGNITION_PORT string + + +func Initialize() error { + cfg_loader, err := configloader.Load(os.Getenv("HI_CONFIG")) + + if err != nil { + return err + } + + RECOGNITION_DB_HOST, err = cfg_loader.Get("RECOGNITION_DB_HOST") + + if err != nil { + return err + } + + RECOGNITION_DB_NAME, err = cfg_loader.Get("RECOGNITION_DB_NAME") + + if err != nil { + return err + } + + RECOGNITION_PORT, err = cfg_loader.Get("RECOGNITION_PORT") + + if err != nil { + return err + } + + return nil +} diff --git a/services/recognition/controller/controller.go b/services/recognition/controller/controller.go new file mode 100644 index 00000000..8fa331cd --- /dev/null +++ b/services/recognition/controller/controller.go @@ -0,0 +1,96 @@ +package controller + +import ( + "encoding/json" + "net/http" + + "github.com/HackIllinois/api/common/errors" + "github.com/HackIllinois/api/common/utils" + "github.com/HackIllinois/api/services/recognition/models" + "github.com/HackIllinois/api/services/recognition/service" + "github.com/gorilla/mux" +) + +func SetupController(route *mux.Route) { + router := route.Subrouter() + + router.HandleFunc("/", CreateRecognition).Methods("POST") + router.HandleFunc("/", GetAllRecognitions).Methods("GET") + + router.HandleFunc("/filter/", GetFilteredRecognitions).Methods("GET") + router.HandleFunc("/{id}/", DeleteRecognition).Methods("DELETE") +} + +/* + Endpoint to delete an recognition with the specified id. + It removes the recognition from the recognition trackers, and every user's tracker. + On successful deletion, it returns the recognition that was deleted. +*/ +func DeleteRecognition(w http.ResponseWriter, r *http.Request) { + id := mux.Vars(r)["id"] + + recognition, err := service.DeleteRecognition(id) + + if err != nil { + errors.WriteError(w, r, errors.InternalError(err.Error(), "Could not delete either the recognition")) + return + } + + json.NewEncoder(w).Encode(recognition) +} + +/* + Endpoint to get all recognitions +*/ +func GetAllRecognitions(w http.ResponseWriter, r *http.Request) { + recognition_list, err := service.GetAllRecognitions() + + if err != nil { + errors.WriteError(w, r, errors.DatabaseError(err.Error(), "Could not get all recognitions.")) + return + } + + json.NewEncoder(w).Encode(recognition_list) +} + +/* + Endpoint to create an recognition +*/ +func CreateRecognition(w http.ResponseWriter, r *http.Request) { + var recognition models.Recognition + json.NewDecoder(r.Body).Decode(&recognition) + + recognition.ID = utils.GenerateUniqueID() + + err := service.CreateRecognition(recognition.ID, recognition) + + if err != nil { + errors.WriteError(w, r, errors.DatabaseError(err.Error(), "Could not create new recognition.")) + return + } + + updated_recognition, err := service.GetRecognition(recognition.ID) + + if err != nil { + errors.WriteError(w, r, errors.DatabaseError(err.Error(), "Could not get updated recognition.")) + return + } + + json.NewEncoder(w).Encode(updated_recognition) +} + + +/* + Endpoint to get recognitions based on filters +*/ +func GetFilteredRecognitions(w http.ResponseWriter, r *http.Request) { + parameters := r.URL.Query() + recognition, err := service.GetFilteredRecognitions(parameters) + + if err != nil { + errors.WriteError(w, r, errors.DatabaseError(err.Error(), "Could not fetch filtered list of recognitions.")) + return + } + + json.NewEncoder(w).Encode(recognition) +} diff --git a/services/recognition/main.go b/services/recognition/main.go new file mode 100644 index 00000000..f335bc05 --- /dev/null +++ b/services/recognition/main.go @@ -0,0 +1,40 @@ +package recognition + +import ( + "github.com/HackIllinois/api/common/apiserver" + "github.com/HackIllinois/api/services/recognition/config" + "github.com/HackIllinois/api/services/recognition/controller" + "github.com/HackIllinois/api/services/recognition/service" + "github.com/gorilla/mux" + "log" +) + +func Initialize() error { + err := config.Initialize() + + if err != nil { + return err + + } + + err = service.Initialize() + + if err != nil { + return err + } + + return nil +} + +func Entry() { + err := Initialize() + + if err != nil { + log.Fatal(err) + } + + router := mux.NewRouter() + controller.SetupController(router.PathPrefix("/recognition")) + + log.Fatal(apiserver.StartServer(config.RECOGNITION_PORT, router, "recognition", Initialize)) +} diff --git a/services/recognition/models/recognition.go b/services/recognition/models/recognition.go new file mode 100644 index 00000000..e79d4ffa --- /dev/null +++ b/services/recognition/models/recognition.go @@ -0,0 +1,16 @@ +package models + +type Recognition struct { + ID string `json:"id" validate:"required"` + Name string `json:"name" validate:"required"` + Description string `json:"description" validate:"required"` + Presenter string `json:"presenter" validate:"required"` + EventID string `json:"eventId" validate:"required"` + Recepients []Recepient `json:"recipients" validate:"required"` + Tags []string `json:"tags"` +} + +type Recepient struct { + Type string `json:"type" validate:"required,oneof=ALL INDIVIDUAL PROJECT"` + TypeID string `json:"typeId"` +} diff --git a/services/recognition/models/recognition_list.go b/services/recognition/models/recognition_list.go new file mode 100644 index 00000000..64a10d7c --- /dev/null +++ b/services/recognition/models/recognition_list.go @@ -0,0 +1,5 @@ +package models + +type RecognitionList struct { + Recognitions []Recognition `json:"recognitions"` +} diff --git a/services/recognition/service/recognition_service.go b/services/recognition/service/recognition_service.go new file mode 100644 index 00000000..c3b88016 --- /dev/null +++ b/services/recognition/service/recognition_service.go @@ -0,0 +1,160 @@ +package service + +import ( + "errors" + + "github.com/HackIllinois/api/common/database" + "github.com/HackIllinois/api/services/recognition/config" + "github.com/HackIllinois/api/services/recognition/models" + "gopkg.in/go-playground/validator.v9" +) + +var validate *validator.Validate + +var db database.Database + +func Initialize() error { + if db != nil { + db.Close() + db = nil + } + + var err error + db, err = database.InitDatabase(config.RECOGNITION_DB_HOST, config.RECOGNITION_DB_NAME) + + if err != nil { + return err + } + + validate = validator.New() + + return nil +} + +/* + Returns the recognition with the given id +*/ +func GetRecognition(id string) (*models.Recognition, error) { + query := database.QuerySelector{ + "id": id, + } + + var recognition models.Recognition + err := db.FindOne("recognitions", query, &recognition) + + if err != nil { + return nil, err + } + + return &recognition, nil +} + +/* + Deletes the recognition with the given id. + Removes the recognition from recognition trackers and every user's tracker. + Returns the recognition that was deleted. +*/ +func DeleteRecognition(id string) (*models.Recognition, error) { + + // Gets recognition to be able to return it later + + recognition, err := GetRecognition(id) + + if err != nil { + return nil, err + } + + query := database.QuerySelector{ + "id": id, + } + + // Remove recognition from recognitions database + err = db.RemoveOne("recognitions", query) + + if err != nil { + return nil, err + } + + // Find all elements, and remove `id` from the Recognitions slice + // All the updates are individually atomic + update_expression := database.QuerySelector { + "$pull": database.QuerySelector{ + "recognitions": id, + }, + } + + _, err = db.UpdateAll("usertrackers", nil, &update_expression) + + return recognition, err +} + + +/* + Returns all the recognitions +*/ +func GetAllRecognitions() (*models.RecognitionList, error) { + recognitions := []models.Recognition{} + // nil implies there are no filters on the query, therefore everything in the "recognitions" collection is returned. + err := db.FindAll("recognitions", nil, &recognitions) + + if err != nil { + return nil, err + } + + recognition_list := models.RecognitionList{ + Recognitions: recognitions, + } + + return &recognition_list, nil +} + + +/* + Returns filtered recognitions +*/ +func GetFilteredRecognitions(parameters map[string][]string) (*models.RecognitionList, error) { + query, err := database.CreateFilterQuery(parameters, models.Recognition{}) + + if err != nil { + return nil, err + } + + recognitions := []models.Recognition{} + filtered_recognitions := models.RecognitionList{Recognitions: recognitions} + err = db.FindAll("recognitions", query, &filtered_recognitions.Recognitions) + + if err != nil { + return nil, err + } + + return &filtered_recognitions, nil +} + + +/* + Creates an recognition with the given id +*/ +func CreateRecognition(id string, recognition models.Recognition) error { + err := validate.Struct(recognition) + + if err != nil { + return err + } + + _, err = GetRecognition(id) + + if err != database.ErrNotFound { + if err != nil { + return err + } + return errors.New("Recognition already exists") + } + + err = db.Insert("recognitions", &recognition) + + if err != nil { + return err + } + + return err +}