diff --git a/Makefile b/Makefile index d8e583fc..8644820a 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ SUBDIRS := $(BASE_DIR)/services/. $(BASE_DIR)/gateway/. $(BASE_DIR)/common/. $(B TARGETS := all test SUBDIRS_TARGETS := $(foreach target,$(TARGETS),$(addsuffix $(target),$(SUBDIRS))) DEPLOY_GATEWAY_TARGETS := gateway -DEPLOY_SERVICE_TARGETS := auth user registration decision rsvp checkin upload mail event stat +DEPLOY_SERVICE_TARGETS := auth user registration decision rsvp checkin upload mail event stat notifications DEPLOY_TARGETS := $(DEPLOY_GATEWAY_TARGETS) $(DEPLOY_SERVICE_TARGETS) .PHONY: $(TARGETS) $(SUBDIRS_TARGETS) diff --git a/common/config/config.go b/common/config/config.go index 8747d7ed..c819e313 100644 --- a/common/config/config.go +++ b/common/config/config.go @@ -6,6 +6,7 @@ import ( ) var IS_PRODUCTION bool +var DEBUG_MODE bool func init() { cfg_loader, err := configloader.Load(os.Getenv("HI_CONFIG")) @@ -21,4 +22,12 @@ func init() { } IS_PRODUCTION = (production == "true") + + debug_mode, err := cfg_loader.Get("DEBUG_MODE") + + if err != nil { + panic(err) + } + + DEBUG_MODE = (debug_mode == "true") } diff --git a/common/errors/AttributeMismatchError.go b/common/errors/AttributeMismatchError.go new file mode 100644 index 00000000..78cf8aac --- /dev/null +++ b/common/errors/AttributeMismatchError.go @@ -0,0 +1,9 @@ +package errors + +import "net/http" + +// Used when the user attempts to perform an action that is not permitted as part of the flow. +// E.g. Attempting to check-in without having RSVPed. +func AttributeMismatchError(raw_error string, message string) ApiError { + return ApiError{Status: http.StatusUnprocessableEntity, Type: "ATTRIBUTE_MISMATCH_ERROR", Message: message, RawError: raw_error} +} diff --git a/common/errors/AuthorizationError.go b/common/errors/AuthorizationError.go new file mode 100644 index 00000000..029c64d9 --- /dev/null +++ b/common/errors/AuthorizationError.go @@ -0,0 +1,8 @@ +package errors + +import "net/http" + +// An error that occurs when an incoming request comes without a JWT token in the Authorization header. +func AuthorizationError(raw_error string, message string) ApiError { + return ApiError{Status: http.StatusForbidden, Type: "AUTHORIZATION_ERROR", Message: message, RawError: raw_error} +} diff --git a/common/errors/DatabaseError.go b/common/errors/DatabaseError.go new file mode 100644 index 00000000..7d032b1a --- /dev/null +++ b/common/errors/DatabaseError.go @@ -0,0 +1,8 @@ +package errors + +import "net/http" + +// An error that occurs when a database operation (e.g. fetch / insert / update) doesn't work. +func DatabaseError(raw_error string, message string) ApiError { + return ApiError{Status: http.StatusInternalServerError, Type: "DATABASE_ERROR", Message: message, RawError: raw_error} +} diff --git a/common/errors/InternalError.go b/common/errors/InternalError.go new file mode 100644 index 00000000..f84df382 --- /dev/null +++ b/common/errors/InternalError.go @@ -0,0 +1,9 @@ +package errors + +import "net/http" + +// Represents errors in the system, including failures in inter-service API calls. +// If there are multiple possible sources of an error, we use this error. +func InternalError(raw_error string, message string) ApiError { + return ApiError{Status: http.StatusInternalServerError, Type: "INTERNAL_ERROR", Message: message, RawError: raw_error} +} diff --git a/common/errors/MalformedRequestError.go b/common/errors/MalformedRequestError.go new file mode 100644 index 00000000..52ec2743 --- /dev/null +++ b/common/errors/MalformedRequestError.go @@ -0,0 +1,8 @@ +package errors + +import "net/http" + +// An error for when struct validation fails, or there are other issues with the payload to an endpoint. +func MalformedRequestError(raw_error string, message string) ApiError { + return ApiError{Status: http.StatusUnprocessableEntity, Type: "MALFORMED_REQUEST_ERROR", Message: message, RawError: raw_error} +} diff --git a/common/errors/UnauthorizedError.go b/common/errors/UnauthorizedError.go deleted file mode 100644 index 9ffca2ff..00000000 --- a/common/errors/UnauthorizedError.go +++ /dev/null @@ -1,5 +0,0 @@ -package errors - -func UnauthorizedError(message string) APIError { - return APIError{Status: 403, Title: "Invalid Authorization", Message: message} -} diff --git a/common/errors/UnknownError.go b/common/errors/UnknownError.go new file mode 100644 index 00000000..e31608c5 --- /dev/null +++ b/common/errors/UnknownError.go @@ -0,0 +1,8 @@ +package errors + +import "net/http" + +// Represents errors in the system whose cause is unidentified. +func UnknownError(raw_error string, message string) ApiError { + return ApiError{Status: http.StatusInternalServerError, Type: "UNKNOWN_ERROR", Message: message, RawError: raw_error} +} diff --git a/common/errors/UnprocessableError.go b/common/errors/UnprocessableError.go deleted file mode 100644 index c12d2732..00000000 --- a/common/errors/UnprocessableError.go +++ /dev/null @@ -1,5 +0,0 @@ -package errors - -func UnprocessableError(message string) APIError { - return APIError{Status: 400, Title: "Unprocessable Request", Message: message} -} diff --git a/common/errors/errors.go b/common/errors/errors.go index cb78604d..aa1863ac 100644 --- a/common/errors/errors.go +++ b/common/errors/errors.go @@ -1,7 +1,16 @@ package errors -type APIError struct { - Status int `json:"status,omitempty"` - Title string `json:"title,omitempty"` - Message string `json:"message,omitempty"` +/** +* Status - the HTTP error code to be sent to the client - should be set by constructor +* Type - the broad category - e.g. DatabaseError, AuthorizationError, InternalError +* Message - provides additional details on the specific error that occurred. +* RawError - the raw error (stringified) that caused the panic. It is only included in the response +* to the client, if the config variable DEBUG_MODE is set to true. In other cases, the +* field is set to the empty string, which causes its omission when encoded to JSON. +**/ +type ApiError struct { + Status int `json:"status"` + Type string `json:"type"` + Message string `json:"message"` + RawError string `json:"raw_error,omitempty"` } diff --git a/common/middleware/error.go b/common/middleware/error.go index 171593f6..7bebf157 100644 --- a/common/middleware/error.go +++ b/common/middleware/error.go @@ -3,6 +3,7 @@ package middleware import ( "encoding/json" "fmt" + "github.com/HackIllinois/api/common/config" "github.com/HackIllinois/api/common/errors" "net/http" ) @@ -18,7 +19,7 @@ func LogError(id string, error_message interface{}) { Error: error_message, } - error_log_message, err := json.Marshal(log_entry) + error_log_message, err := json.MarshalIndent(log_entry, "", " ") if err != nil { fmt.Printf("Failed to marshal error for id: %v\n", id) @@ -31,21 +32,39 @@ func LogError(id string, error_message interface{}) { func ErrorMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer func() { - if rec := recover(); rec != nil { + if panic_object := recover(); panic_object != nil { w.Header().Set("Content-Type", "application/json") - switch panic_error := rec.(type) { - case errors.APIError: - LogError(r.Header.Get("HackIllinois-Identity"), panic_error) - w.WriteHeader(panic_error.Status) - json.NewEncoder(w).Encode(panic_error) + switch panic_error := panic_object.(type) { + case errors.ApiError: + handleApiError(panic_error, w, r) default: - LogError(r.Header.Get("HackIllinois-Identity"), rec) - w.WriteHeader(http.StatusInternalServerError) - json.NewEncoder(w).Encode(errors.APIError{Status: 500, Message: "Unknown Error"}) + handleUnknownError(panic_error, w, r) } } }() - next.ServeHTTP(w, r) }) } + +func handleApiError(err errors.ApiError, w http.ResponseWriter, r *http.Request) { + LogError(r.Header.Get("HackIllinois-Identity"), err) + + w.WriteHeader(err.Status) + + // Strip the raw error string if we're not in debug mode + if config.DEBUG_MODE { + err.RawError = "" + } + + json.NewEncoder(w).Encode(err) +} + +func handleUnknownError(err interface{}, w http.ResponseWriter, r *http.Request) { + LogError(r.Header.Get("HackIllinois-Identity"), err) + + w.WriteHeader(http.StatusInternalServerError) + + err_string := fmt.Sprintf("%v", err) + + json.NewEncoder(w).Encode(errors.UnknownError(err_string, err_string)) +} diff --git a/config/dev_config.json b/config/dev_config.json index bd70453a..09bc99ed 100644 --- a/config/dev_config.json +++ b/config/dev_config.json @@ -9,6 +9,7 @@ "MAIL_SERVICE": "http://localhost:8009", "EVENT_SERVICE": "http://localhost:8010", "STAT_SERVICE": "http://localhost:8011", + "NOTIFICATIONS_SERVICE": "http://localhost:8012", "GATEWAY_PORT": "8000", "AUTH_PORT": ":8002", @@ -21,6 +22,7 @@ "MAIL_PORT": ":8009", "EVENT_PORT": ":8010", "STAT_PORT": ":8011", + "NOTIFICATIONS_PORT": ":8012", "AUTH_DB_HOST": "localhost", "USER_DB_HOST": "localhost", @@ -32,6 +34,7 @@ "MAIL_DB_HOST": "localhost", "EVENT_DB_HOST": "localhost", "STAT_DB_HOST": "localhost", + "NOTIFICATIONS_DB_HOST": "localhost", "AUTH_DB_NAME": "auth", "USER_DB_NAME": "user", @@ -43,10 +46,16 @@ "MAIL_DB_NAME": "mail", "EVENT_DB_NAME": "event", "STAT_DB_NAME": "stat", + "NOTIFICATIONS_DB_NAME": "notifications", "S3_REGION": "us-east-1", "S3_BUCKET": "hackillinois-upload-2019", + "SNS_REGION": "us-east-1", + + "ANDROID_PLATFORM_ARN": "", + "IOS_PLATFORM_ARN": "", + "STAFF_DOMAIN": "hackillinois.org", "SYSTEM_ADMIN_EMAIL": "systems@hackillinois.org", @@ -65,6 +74,8 @@ "IS_PRODUCTION": "false", + "DEBUG_MODE": "true", + "DECISION_EXPIRATION_HOURS": "48", "STAT_ENDPOINTS": { diff --git a/config/production_config.json b/config/production_config.json index 5f6b658d..ad0a4bfc 100644 --- a/config/production_config.json +++ b/config/production_config.json @@ -21,6 +21,7 @@ "MAIL_PORT": ":8009", "EVENT_PORT": ":8010", "STAT_PORT": ":8011", + "NOTIFICATIONS_PORT": ":8012", "AUTH_DB_NAME": "auth", "USER_DB_NAME": "user", @@ -32,10 +33,13 @@ "MAIL_DB_NAME": "mail", "EVENT_DB_NAME": "event", "STAT_DB_NAME": "stat", + "NOTIFICATIONS_DB_NAME": "notifications", "S3_REGION": "us-east-1", "S3_BUCKET": "hackillinois-upload-2019", + "SNS_REGION": "us-east-1", + "STAFF_DOMAIN": "hackillinois.org", "SYSTEM_ADMIN_EMAIL": "systems@hackillinois.org", @@ -44,6 +48,8 @@ "IS_PRODUCTION": "true", + "DEBUG_MODE": "false", + "DECISION_EXPIRATION_HOURS": "48", "STAT_ENDPOINTS": { diff --git a/config/test_config.json b/config/test_config.json index 23875572..e40f1b4b 100644 --- a/config/test_config.json +++ b/config/test_config.json @@ -21,6 +21,7 @@ "MAIL_PORT": ":8009", "EVENT_PORT": ":8010", "STAT_PORT": ":8011", + "NOTIFICATIONS_PORT": ":8012", "AUTH_DB_HOST": "localhost", "USER_DB_HOST": "localhost", @@ -32,6 +33,7 @@ "MAIL_DB_HOST": "localhost", "EVENT_DB_HOST": "localhost", "STAT_DB_HOST": "localhost", + "NOTIFICATIONS_DB_HOST": "localhost", "AUTH_DB_NAME": "test-auth", "USER_DB_NAME": "test-user", @@ -43,10 +45,16 @@ "MAIL_DB_NAME": "test-mail", "EVENT_DB_NAME": "test-event", "STAT_DB_NAME": "test-stat", + "NOTIFICATIONS_DB_NAME": "test-notifications", "S3_REGION": "us-east-1", "S3_BUCKET": "hackillinois-upload-2019", + "SNS_REGION": "us-east-1", + + "ANDROID_PLATFORM_ARN": "", + "IOS_PLATFORM_ARN": "", + "STAFF_DOMAIN": "hackillinois.org", "SYSTEM_ADMIN_EMAIL": "systems@hackillinois.org", @@ -65,6 +73,8 @@ "IS_PRODUCTION": "false", + "DEBUG_MODE": "true", + "DECISION_EXPIRATION_HOURS": "48", "STAT_ENDPOINTS": { diff --git a/documentation/docs/reference/introduction.md b/documentation/docs/reference/introduction.md index 6545f98d..e866f096 100644 --- a/documentation/docs/reference/introduction.md +++ b/documentation/docs/reference/introduction.md @@ -6,8 +6,26 @@ Each microservice is responsible for doing only one set of tasks. For example, o For authorization we use [JSON Web Tokens (JWTs)](https://jwt.io) that encode a user ID and some more information, in a system similar to [Bearer (or token-based) authentication](https://swagger.io/docs/specification/authentication/bearer-authentication/). -When a client makes an HTTP request to `api.hackillinois.org`, it is taken through several middleware. One of them is the *Authentication* middleware, which ensure the user is authenticated. Another one is the *Identification* middleware, and puts the user ID of the requesting user in the HackIllinois-Identity header, which can be used by the individual services. +When a client makes an HTTP request to `api.hackillinois.org`, it is taken through several middleware. One of them is the *Authentication* middleware, which ensures the user is authenticated. Another one is the *Identification* middleware, and puts the user ID of the requesting user in the HackIllinois-Identity header, which can be used by the individual services. The *Error* middleware allows passing of errors to the client using standard HTTP mechanisms, such as status codes, and response bodies. The authorized request is then forwarded to the relevant micro-service based on routes configured in the [gateway](/reference/gateway), where controllers present in each micro-service process the request, call various service funcitons, perform the action requested, and return the response, which is passed back to the user, via Arbor. -Our persistence layer consists of a [MongoDB](https://mongodb.com) database, which has collections storing data relevant to each service. \ No newline at end of file +Our persistence layer consists of a [MongoDB](https://mongodb.com) database, which has collections storing data relevant to each service. + +## Errors + +Setting the DEBUG_MODE to "true" in the config file allows raw error messages (if applicable) to be passed through to the client. Otherwise, the raw error is suppressed. + +Errors are classified into the following types: + +1. **DatabaseError** - When database operations, such as fetch / insert / update) doesn't work. These are usually returned when a document / record that was requested wasn't found, such as when an operation is performed on an inexistent user. + +2. **MalformedRequestError** - When the request is invalid or missing some key information. Possible scenarios are, when field validation fails on a request body, or when an ID is missing for an endpoint that depends on it. + +3. **AuthorizationError** - When an authentication / authorization attempt fails. Possible scenarios include when OAuth-related services fail, such as when an authorization code is incorrect, a token is invalid / has expired etc. + +4. **AttributeMismatchError** - When an action is performed on a user who is missing some attribute, such as when a check-in (without override) is attempted for a user who doesn't have a registration or RSVP, modifying a decision on a candidate (hacker) whose decision has already been finalized by a senior staff member etc. + +5. **InternalError** - When there could be multiple possible causes of the error, this is what we use. Using DEBUG_MODE to get the raw error is highly recommended to expedite bug resolution. + +6. **UnknownError** - When the cause of an error cannot be identified. diff --git a/documentation/docs/reference/services/Authorization.md b/documentation/docs/reference/services/Authorization.md index 07c37b41..cfa912c0 100644 --- a/documentation/docs/reference/services/Authorization.md +++ b/documentation/docs/reference/services/Authorization.md @@ -35,7 +35,7 @@ Response format: } ``` GET /auth/roles/USERID/ --------------------------- +----------------------- Gets the roles of the user with the id `USERID`. @@ -50,6 +50,7 @@ Response format: ``` PUT /auth/roles/add/ +-------------------- Adds the given `role` to the user with the given `id`. The updated user's roles will be returned. @@ -74,6 +75,7 @@ Response format: ``` PUT /auth/roles/remove/ +----------------------- Removes the given `role` from the user with the given `id`. The updated user's roles will be returned. @@ -96,7 +98,7 @@ Response format: ``` GET /auth/token/refresh/ ------------------ +------------------------ Creates a new JWT for the current user. This is useful when the user's roles change, and the updated roles need to be encoded into a new JWT, such as during registration. diff --git a/documentation/docs/reference/services/Notifications.md b/documentation/docs/reference/services/Notifications.md new file mode 100644 index 00000000..535d275c --- /dev/null +++ b/documentation/docs/reference/services/Notifications.md @@ -0,0 +1,220 @@ +# Notifications + +## GET /notifications/ + +Returns a list of all notification topics. + +Response format: + +``` +{ + "topics": [ + "Mentors", + "Attendees" + ] +} +``` + +## POST /notifications/ + +Creates a new topic with the requested name. Returns the created topic. + +Request format: + +``` +{ + "name": "Mentors" +} +``` + +Response format: + +``` +{ + "name": "Mentors" +} +``` + +## GET /notifications/all/ + +Returns a list of all past notifications. + +Response format: + +``` +{ + "notifications": [ + { + "title": "Notification 1", + "body": "This is a notification!", + "time": 1541037801, + "topicName": "Attendee" + }, + { + "title": "Notification 2", + "body": "This is another notification!", + "time": 1541069201, + "topicName": "Attendee" + }, + { + "title": "Notification 3", + "body": "This is another notification, for another topic!", + "time": 1541169201, + "topicName": "Mentor" + } + ] +} +``` + +## GET /notifications/TOPICNAME/ + +Returns a list of all past notifications for a given topic `TOPICNAME`. + +Response format: + +``` +{ + "notifications": [ + { + "title": "Notification 1", + "body": "This is a notification!", + "time": 1541037801, + "topicName": "Attendee" + }, + { + "title": "Notification 2", + "body": "This is another notification!", + "time": 1541069201, + "topicName": "Attendee" + } + ] +} +``` + +## DELETE /notifications/TOPICNAME/ + +Delete a topic with name `TOPICNAME`. Returns a list of all remaining topics. + +Response format: + +``` +{ + "topics": [ + "Mentors", + "Attendees" + ] +} +``` + +## POST /notifications/TOPICNAME/ + +Publishes and distributes a notification to all users subscribed to the topic `TOPICNAME`. Returns the created notification. + +Request format: + +``` +{ + "topic": "Message topic", + "body": "Message to send to users" +} +``` + +Response format: + +``` +{ + "topic": "Message topic", + "body": "Message to send to users", + "time": 1541644690, + "topicName": "Attendee" +} +``` + +## GET /notifications/TOPICNAME/info/ + +Gets information associated by the topic `TOPICNAME`. + +Response format: + +``` +{ + "name": "Mentors", + "userIds": [ + "testuser1" + ] +} +``` + +## POST /notifications/TOPICNAME/add/ + +Modifies the topic `TOPICNAME`, subscribing the users in the list `userIds`. + +Request format: + +``` +{ + "userIds": [ + "testuser1", + "testuser2" + ] +} +``` + +Response format: + +``` +{ + "name": "Mentors", + "userIds": [ + "testuser1" + ] +} +``` + +## POST /notifications/TOPICNAME/remove/ + +Modifies the topic `TOPICNAME`, unsubscribing the users in the list `userIds`. + +Request format: + +``` +{ + "userIds": [ + "testuser1", + ] +} +``` + +Response format: + +``` +{ + "name": "Mentors", + "userIds": [ + "testuser2" + ] +} +``` + +## POST /notifications/devices/ + +Associates the device specified by the provided device token with the current user. +Valid platforms are `android` and `ios`. + +Request format: + +``` +{ + "deviceToken": "abcdef", + "platform": "android" +} +``` + +Response format: + +``` +{ + "deviceToken": "abcdef", + "platform": "android" +} +``` diff --git a/documentation/mkdocs.yml b/documentation/mkdocs.yml index f91d993d..bf2fed06 100644 --- a/documentation/mkdocs.yml +++ b/documentation/mkdocs.yml @@ -11,7 +11,7 @@ nav: - 'License': 'about/License.md' - Getting Started: - 'Developer Guide': 'getting-started/developer-guide.md' -- Reference: +- Reference: - Introduction: 'reference/introduction.md' - Services: - 'Authorization': 'reference/services/Authorization.md' @@ -19,6 +19,7 @@ nav: - 'Decision': 'reference/services/Decision.md' - 'Event': 'reference/services/Event.md' - 'Mail': 'reference/services/Mail.md' + - 'Notifications': 'reference/services/Notifications.md' - 'RSVP': 'reference/services/RSVP.md' - 'Registration': 'reference/services/Registration.md' - 'Statistics': 'reference/services/Statistics.md' @@ -31,4 +32,4 @@ nav: - 'Documentation System': 'reference/api-writers/documentation-system.md' theme: - name: 'material' \ No newline at end of file + name: 'material' diff --git a/gateway/config/config.go b/gateway/config/config.go index 099d68b6..24417452 100644 --- a/gateway/config/config.go +++ b/gateway/config/config.go @@ -22,6 +22,7 @@ var UPLOAD_SERVICE string var MAIL_SERVICE string var EVENT_SERVICE string var STAT_SERVICE string +var NOTIFICATIONS_SERVICE string func init() { @@ -97,6 +98,12 @@ func init() { panic(err) } + NOTIFICATIONS_SERVICE, err = cfg_loader.Get("NOTIFICATIONS_SERVICE") + + if err != nil { + panic(err) + } + port_str, err := cfg_loader.Get("GATEWAY_PORT") if err != nil { diff --git a/gateway/services/health.go b/gateway/services/health.go index 1f83b893..7afc03de 100644 --- a/gateway/services/health.go +++ b/gateway/services/health.go @@ -13,16 +13,17 @@ import ( ) var ServiceLocations = map[string]string{ - "auth": config.AUTH_SERVICE, - "user": config.USER_SERVICE, - "registration": config.REGISTRATION_SERVICE, - "decision": config.DECISION_SERVICE, - "rsvp": config.RSVP_SERVICE, - "checkin": config.CHECKIN_SERVICE, - "upload": config.UPLOAD_SERVICE, - "mail": config.MAIL_SERVICE, - "event": config.EVENT_SERVICE, - "stat": config.STAT_SERVICE, + "auth": config.AUTH_SERVICE, + "user": config.USER_SERVICE, + "registration": config.REGISTRATION_SERVICE, + "decision": config.DECISION_SERVICE, + "rsvp": config.RSVP_SERVICE, + "checkin": config.CHECKIN_SERVICE, + "upload": config.UPLOAD_SERVICE, + "mail": config.MAIL_SERVICE, + "event": config.EVENT_SERVICE, + "stat": config.STAT_SERVICE, + "notifications": config.NOTIFICATIONS_SERVICE, } var HealthRoutes = arbor.RouteCollection{ diff --git a/gateway/services/notifications.go b/gateway/services/notifications.go new file mode 100644 index 00000000..5d4cf04d --- /dev/null +++ b/gateway/services/notifications.go @@ -0,0 +1,118 @@ +package services + +import ( + "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" + "net/http" +) + +var NotificationsURL = config.NOTIFICATIONS_SERVICE + +const NotificationsFormat string = "JSON" + +var NotificationsRoutes = arbor.RouteCollection{ + arbor.Route{ + "GetAllTopics", + "GET", + "/notifications/", + alice.New(middleware.IdentificationMiddleware, middleware.AuthMiddleware([]models.Role{models.AdminRole, models.StaffRole})).ThenFunc(GetAllTopics).ServeHTTP, + }, + arbor.Route{ + "GetAllNotifications", + "GET", + "/notifications/all/", + alice.New(middleware.IdentificationMiddleware, middleware.AuthMiddleware([]models.Role{models.UserRole})).ThenFunc(GetAllNotifications).ServeHTTP, + }, + arbor.Route{ + "CreateTopic", + "POST", + "/notifications/", + alice.New(middleware.IdentificationMiddleware, middleware.AuthMiddleware([]models.Role{models.AdminRole})).ThenFunc(CreateTopic).ServeHTTP, + }, + arbor.Route{ + "RegisterDeviceToUser", + "POST", + "/notifications/device/", + alice.New(middleware.IdentificationMiddleware, middleware.AuthMiddleware([]models.Role{models.UserRole})).ThenFunc(RegisterDeviceToUser).ServeHTTP, + }, + arbor.Route{ + "GetNotificationsForTopic", + "GET", + "/notifications/{id}/", + alice.New(middleware.IdentificationMiddleware, middleware.AuthMiddleware([]models.Role{models.UserRole})).ThenFunc(GetNotificationsForTopic).ServeHTTP, + }, + arbor.Route{ + "DeleteTopic", + "DELETE", + "/notifications/{id}/", + alice.New(middleware.IdentificationMiddleware, middleware.AuthMiddleware([]models.Role{models.AdminRole})).ThenFunc(DeleteTopic).ServeHTTP, + }, + arbor.Route{ + "PublishNotification", + "POST", + "/notifications/{id}/", + alice.New(middleware.IdentificationMiddleware, middleware.AuthMiddleware([]models.Role{models.AdminRole})).ThenFunc(PublishNotification).ServeHTTP, + }, + arbor.Route{ + "AddUsersToTopic", + "POST", + "/notifications/{id}/add/", + alice.New(middleware.IdentificationMiddleware, middleware.AuthMiddleware([]models.Role{models.AdminRole, models.StaffRole})).ThenFunc(AddUsersToTopic).ServeHTTP, + }, + arbor.Route{ + "RemoveUsersFromTopic", + "POST", + "/notifications/{id}/remove/", + alice.New(middleware.IdentificationMiddleware, middleware.AuthMiddleware([]models.Role{models.AdminRole, models.StaffRole})).ThenFunc(RemoveUsersFromTopic).ServeHTTP, + }, + arbor.Route{ + "GetTopicInfo", + "GET", + "/notifications/{id}/info/", + alice.New(middleware.IdentificationMiddleware, middleware.AuthMiddleware([]models.Role{models.AdminRole})).ThenFunc(GetTopicInfo).ServeHTTP, + }, +} + +func GetAllTopics(w http.ResponseWriter, r *http.Request) { + arbor.GET(w, NotificationsURL+r.URL.String(), NotificationsFormat, "", r) +} + +func GetAllNotifications(w http.ResponseWriter, r *http.Request) { + arbor.GET(w, NotificationsURL+r.URL.String(), NotificationsFormat, "", r) +} + +func CreateTopic(w http.ResponseWriter, r *http.Request) { + arbor.POST(w, NotificationsURL+r.URL.String(), NotificationsFormat, "", r) +} + +func GetTopicInfo(w http.ResponseWriter, r *http.Request) { + arbor.GET(w, NotificationsURL+r.URL.String(), NotificationsFormat, "", r) +} + +func DeleteTopic(w http.ResponseWriter, r *http.Request) { + arbor.DELETE(w, NotificationsURL+r.URL.String(), NotificationsFormat, "", r) +} + +func PublishNotification(w http.ResponseWriter, r *http.Request) { + arbor.POST(w, NotificationsURL+r.URL.String(), NotificationsFormat, "", r) +} + +func GetNotificationsForTopic(w http.ResponseWriter, r *http.Request) { + arbor.GET(w, NotificationsURL+r.URL.String(), NotificationsFormat, "", r) +} + +func AddUsersToTopic(w http.ResponseWriter, r *http.Request) { + arbor.POST(w, NotificationsURL+r.URL.String(), NotificationsFormat, "", r) +} + +func RemoveUsersFromTopic(w http.ResponseWriter, r *http.Request) { + arbor.POST(w, NotificationsURL+r.URL.String(), NotificationsFormat, "", r) +} + +func RegisterDeviceToUser(w http.ResponseWriter, r *http.Request) { + arbor.POST(w, NotificationsURL+r.URL.String(), NotificationsFormat, "", r) +} diff --git a/gateway/services/services.go b/gateway/services/services.go index 2c5da5fe..276fd859 100644 --- a/gateway/services/services.go +++ b/gateway/services/services.go @@ -42,6 +42,7 @@ func RegisterAPIs() arbor.RouteCollection { Routes = append(Routes, EventRoutes...) Routes = append(Routes, StatRoutes...) Routes = append(Routes, HealthRoutes...) + Routes = append(Routes, NotificationsRoutes...) return Routes } diff --git a/release/env.template b/release/env.template index a8fd087f..d51ba5fd 100644 --- a/release/env.template +++ b/release/env.template @@ -20,6 +20,7 @@ UPLOAD_DB_HOST= MAIL_DB_HOST= EVENT_DB_HOST= STAT_DB_HOST= +NOTIFICATIONS_DB_HOST= # Set the oauth client id and secret for your GitHub, Google, and Linkedin applications GITHUB_CLIENT_ID= diff --git a/release/start.sh b/release/start.sh index 67326361..c49cd891 100755 --- a/release/start.sh +++ b/release/start.sh @@ -11,4 +11,5 @@ ./hackillinois-api-mail & ./hackillinois-api-event & ./hackillinois-api-stat & +./hackillinois-api-notifications & ./hackillinois-api-gateway -u diff --git a/scripts/run.sh b/scripts/run.sh index 37789964..0c25ef57 100755 --- a/scripts/run.sh +++ b/scripts/run.sh @@ -24,6 +24,7 @@ hackillinois-api-upload & hackillinois-api-mail & hackillinois-api-event & hackillinois-api-stat & +hackillinois-api-notifications & hackillinois-api-gateway -u & diff --git a/services/auth/controller/controller.go b/services/auth/controller/controller.go index 52729b53..b39368bf 100644 --- a/services/auth/controller/controller.go +++ b/services/auth/controller/controller.go @@ -38,7 +38,7 @@ func Authorize(w http.ResponseWriter, r *http.Request) { oauth_authorization_url, err := service.GetAuthorizeRedirect(provider, client_application_url) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.AuthorizationError(err.Error(), "Could not retrieve OAuth provider authorization code URL.")) } http.Redirect(w, r, oauth_authorization_url, 302) @@ -63,69 +63,69 @@ func Login(w http.ResponseWriter, r *http.Request) { oauth_token, err := service.GetOauthToken(oauth_code.Code, provider, client_application_url) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.AuthorizationError(err.Error(), "Could not get OAuth token.")) } email, is_email_verified, err := service.GetEmail(oauth_token, provider) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.AuthorizationError(err.Error(), "Could not fetch user's email from OAuth provider.")) } id, err := service.GetUniqueId(oauth_token, provider) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.AuthorizationError(err.Error(), "Could not fetch user's unique ID from OAuth provider.")) } roles, err := service.GetUserRoles(id, true) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.AuthorizationError(err.Error(), "Could not fetch user's API roles.")) } if is_email_verified { err = service.AddAutomaticRoleGrants(id, email) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.AuthorizationError(err.Error(), "Could not automatically grant roles to user (based on verified email domain).")) } roles, err = service.GetUserRoles(id, false) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.AuthorizationError(err.Error(), "Could not determine user roles, after automatic role grants.")) } } signed_token, err := service.MakeToken(id, email, roles) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.AuthorizationError(err.Error(), "Could not create HackIllinois API JWT for user.")) } username, err := service.GetUsername(oauth_token, provider) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.AuthorizationError(err.Error(), "Could not fetch user's username from OAuth provider.")) } first_name, err := service.GetFirstName(oauth_token, provider) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.AuthorizationError(err.Error(), "Could not get user's first name from OAuth provider.")) } last_name, err := service.GetLastName(oauth_token, provider) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.AuthorizationError(err.Error(), "Could not get user's last name from OAuth provider.")) } err = service.SendUserInfo(id, username, first_name, last_name, email) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.InternalError(err.Error(), "Could not send user information to user service.")) } token := models.Token{ @@ -142,13 +142,13 @@ func GetRoles(w http.ResponseWriter, r *http.Request) { id := mux.Vars(r)["id"] if id == "" { - panic(errors.UnprocessableError("Must provide id parameter")) + panic(errors.MalformedRequestError("Must provide id parameter in request.", "Must provide id parameter in request.")) } roles, err := service.GetUserRoles(id, false) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.AuthorizationError(err.Error(), "Could not get user's roles.")) } user_roles := models.UserRoles{ @@ -167,19 +167,19 @@ func AddRole(w http.ResponseWriter, r *http.Request) { json.NewDecoder(r.Body).Decode(&role_modification) if role_modification.ID == "" { - panic(errors.UnprocessableError("Must provide id parameter")) + panic(errors.MalformedRequestError("Must provide id parameter in request.", "Must provide id parameter in request.")) } err := service.AddUserRole(role_modification.ID, role_modification.Role) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.InternalError(err.Error(), "Could not add user role.")) } roles, err := service.GetUserRoles(role_modification.ID, false) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.InternalError(err.Error(), "Could not get user's roles.")) } updated_roles := models.UserRoles{ @@ -198,19 +198,19 @@ func RemoveRole(w http.ResponseWriter, r *http.Request) { json.NewDecoder(r.Body).Decode(&role_modification) if role_modification.ID == "" { - panic(errors.UnprocessableError("Must provide id parameter")) + panic(errors.MalformedRequestError("Must provide id parameter in request.", "Must provide id parameter in request.")) } err := service.RemoveUserRole(role_modification.ID, role_modification.Role) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.InternalError(err.Error(), "Could not remove user's user role.")) } roles, err := service.GetUserRoles(role_modification.ID, false) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.InternalError(err.Error(), "Could not fetch user's roles.")) } updated_roles := models.UserRoles{ @@ -233,7 +233,7 @@ func RefreshToken(w http.ResponseWriter, r *http.Request) { user_info, err := service.GetUserInfo(id) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Could not fetch user info.")) } email := user_info.Email @@ -243,7 +243,7 @@ func RefreshToken(w http.ResponseWriter, r *http.Request) { roles, err := service.GetUserRoles(id, false) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.InternalError(err.Error(), "Could not fetch user roles.")) } // Create the new token using user ID, email, and (updated) roles. @@ -251,7 +251,7 @@ func RefreshToken(w http.ResponseWriter, r *http.Request) { signed_token, err := service.MakeToken(id, email, roles) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.AuthorizationError(err.Error(), "Could not make a new JWT for the user.")) } new_token := models.Token{ diff --git a/services/checkin/controller/controller.go b/services/checkin/controller/controller.go index 9084758c..966a4dd7 100644 --- a/services/checkin/controller/controller.go +++ b/services/checkin/controller/controller.go @@ -31,7 +31,7 @@ func GetUserCheckin(w http.ResponseWriter, r *http.Request) { user_checkin, err := service.GetUserCheckin(id) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Could not get specified user's check-in details.")) } json.NewEncoder(w).Encode(user_checkin) @@ -46,7 +46,7 @@ func GetCurrentUserCheckin(w http.ResponseWriter, r *http.Request) { user_checkin, err := service.GetUserCheckin(id) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Could not get current user's check-in details.")) } json.NewEncoder(w).Encode(user_checkin) @@ -62,30 +62,30 @@ func CreateUserCheckin(w http.ResponseWriter, r *http.Request) { can_user_checkin, err := service.CanUserCheckin(user_checkin.ID, user_checkin.Override) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.InternalError(err.Error(), "Unable to determine user's check-in permissions.")) } if !can_user_checkin { - panic(errors.UnprocessableError("Attendee must be RSVPed to check-in (or have a staff override).")) + panic(errors.AttributeMismatchError("Reasons for not being able to check-in include: no RSVP, no staff override (in case of no RSVP), or check-ins are not allowed at this time.", "Attendee is not allowed to check-in.")) } err = service.CreateUserCheckin(user_checkin.ID, user_checkin) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Could not create user check-in.")) } updated_checkin, err := service.GetUserCheckin(user_checkin.ID) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Could not get recently created check-in information.")) } if updated_checkin.Override { err = service.AddAttendeeRole(updated_checkin.ID) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.AuthorizationError(err.Error(), "Could not add attendee role to user.")) } } @@ -102,20 +102,20 @@ func UpdateUserCheckin(w http.ResponseWriter, r *http.Request) { err := service.UpdateUserCheckin(user_checkin.ID, user_checkin) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Could not update user check-in information.")) } updated_checkin, err := service.GetUserCheckin(user_checkin.ID) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Could not fetch updated check-in information.")) } if updated_checkin.Override { err = service.AddAttendeeRole(updated_checkin.ID) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.AuthorizationError(err.Error(), "Could not add attendee role.")) } } @@ -129,7 +129,7 @@ func GetAllCheckedInUsers(w http.ResponseWriter, r *http.Request) { checked_in_users, err := service.GetAllCheckedInUsers() if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Could not get all checked-in users.")) } json.NewEncoder(w).Encode(checked_in_users) @@ -142,7 +142,7 @@ func GetStats(w http.ResponseWriter, r *http.Request) { stats, err := service.GetStats() if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.InternalError(err.Error(), "Could not get check-in service statistics.")) } json.NewEncoder(w).Encode(stats) diff --git a/services/decision/controller/controller.go b/services/decision/controller/controller.go index dbba3780..8e6cc283 100644 --- a/services/decision/controller/controller.go +++ b/services/decision/controller/controller.go @@ -33,7 +33,7 @@ func GetCurrentDecision(w http.ResponseWriter, r *http.Request) { decision, err := service.GetDecision(id) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Could not get current user's decision.")) } decision_view := models.DecisionView{ @@ -58,24 +58,24 @@ func UpdateDecision(w http.ResponseWriter, r *http.Request) { json.NewDecoder(r.Body).Decode(&decision) if decision.ID == "" { - panic(errors.UnprocessableError("Must provide id parameter.")) + panic(errors.MalformedRequestError("Must provide id parameter in request.", "Must provide id parameter in request.")) } has_decision, err := service.HasDecision(decision.ID) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Could not determine user's decision.")) } if has_decision { existing_decision_history, err := service.GetDecision(decision.ID) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Could not get current user's existing decision history.")) } if existing_decision_history.Finalized { - panic(errors.UnprocessableError("Cannot modify finalized decisions.")) + panic(errors.AttributeMismatchError("Cannot modify finalized decisions.", "Cannot modify finalized decisions.")) } } @@ -87,13 +87,13 @@ func UpdateDecision(w http.ResponseWriter, r *http.Request) { err = service.UpdateDecision(decision.ID, decision) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.InternalError(err.Error(), "Could not update decision.")) } updated_decision, err := service.GetDecision(decision.ID) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Could not fetch updated decision.")) } json.NewEncoder(w).Encode(updated_decision) @@ -110,7 +110,7 @@ func FinalizeDecision(w http.ResponseWriter, r *http.Request) { id := decision_finalized.ID if id == "" { - panic(errors.UnprocessableError("Must provide id parameter to retrieve current decision ")) + panic(errors.MalformedRequestError("Must provide id parameter to retrieve current decision.", "Must provide id parameter to retrieve current decision.")) } // Assuming we are working on the specified user's decision @@ -129,23 +129,23 @@ func FinalizeDecision(w http.ResponseWriter, r *http.Request) { err := service.UpdateDecision(id, latest_decision) if err != nil { - panic(errors.UnprocessableError("Error updating the decision, in an attempt to finalize it.")) + panic(errors.InternalError(err.Error(), "Error updating the decision, in an attempt to finalize it.")) } } else { - panic(errors.UnprocessableError("Decision already finalized.")) + panic(errors.AttributeMismatchError("Decision already finalized.", "Decision already finalized.")) } updated_decision, err := service.GetDecision(id) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Could not fetch updated decision.")) } if updated_decision.Finalized { err = service.AddUserToMailList(id, updated_decision) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.InternalError(err.Error(), "Could not add user to mail list.")) } } @@ -160,7 +160,7 @@ func GetFilteredDecisions(w http.ResponseWriter, r *http.Request) { decisions, err := service.GetFilteredDecisions(parameters) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Could not retrieve filtered decisions.")) } json.NewEncoder(w).Encode(decisions) @@ -175,7 +175,7 @@ func GetDecision(w http.ResponseWriter, r *http.Request) { decision, err := service.GetDecision(id) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Could not get decision for the specified user.")) } json.NewEncoder(w).Encode(decision) @@ -188,7 +188,7 @@ func GetStats(w http.ResponseWriter, r *http.Request) { stats, err := service.GetStats() if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.InternalError(err.Error(), "Could not get decision service statistics.")) } json.NewEncoder(w).Encode(stats) diff --git a/services/event/controller/controller.go b/services/event/controller/controller.go index 248916dc..e27f5bec 100644 --- a/services/event/controller/controller.go +++ b/services/event/controller/controller.go @@ -40,7 +40,7 @@ func GetEvent(w http.ResponseWriter, r *http.Request) { event, err := service.GetEvent(name) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Could not fetch the event details.")) } json.NewEncoder(w).Encode(event) @@ -57,7 +57,7 @@ func DeleteEvent(w http.ResponseWriter, r *http.Request) { event, err := service.DeleteEvent(name) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.InternalError(err.Error(), "Could not delete either the event, event trackers, or user trackers, or an intermediary subroutine failed.")) } json.NewEncoder(w).Encode(event) @@ -70,7 +70,7 @@ func GetAllEvents(w http.ResponseWriter, r *http.Request) { event_list, err := service.GetAllEvents() if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Could not get all events.")) } json.NewEncoder(w).Encode(event_list) @@ -86,13 +86,13 @@ func CreateEvent(w http.ResponseWriter, r *http.Request) { err := service.CreateEvent(event.Name, event) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Could not create new event.")) } updated_event, err := service.GetEvent(event.Name) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Could not get updated event.")) } json.NewEncoder(w).Encode(updated_event) @@ -108,13 +108,13 @@ func UpdateEvent(w http.ResponseWriter, r *http.Request) { err := service.UpdateEvent(event.Name, event) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Could not update the event.")) } updated_event, err := service.GetEvent(event.Name) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Could not get updated event details.")) } json.NewEncoder(w).Encode(updated_event) @@ -129,7 +129,7 @@ func GetEventTrackingInfo(w http.ResponseWriter, r *http.Request) { tracker, err := service.GetEventTracker(name) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Could not get event tracker.")) } json.NewEncoder(w).Encode(tracker) @@ -144,7 +144,7 @@ func GetUserTrackingInfo(w http.ResponseWriter, r *http.Request) { tracker, err := service.GetUserTracker(id) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Could not get user tracker.")) } json.NewEncoder(w).Encode(tracker) @@ -160,29 +160,29 @@ func MarkUserAsAttendingEvent(w http.ResponseWriter, r *http.Request) { is_checkedin, err := service.IsUserCheckedIn(tracking_info.UserID) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.InternalError(err.Error(), "Could not determine check-in status of user.")) } if !is_checkedin { - panic(errors.UnprocessableError("User must be checked in to attend event")) + panic(errors.AttributeMismatchError("User must be checked-in to attend event.", "User must be checked-in to attend event.")) } err = service.MarkUserAsAttendingEvent(tracking_info.EventName, tracking_info.UserID) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.InternalError(err.Error(), "Could not mark user as attending the event.")) } event_tracker, err := service.GetEventTracker(tracking_info.EventName) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Could not get event trackers.")) } user_tracker, err := service.GetUserTracker(tracking_info.UserID) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Could not get user trackers.")) } tracking_status := &models.TrackingStatus{ @@ -202,7 +202,7 @@ func GetEventFavorites(w http.ResponseWriter, r *http.Request) { favorites, err := service.GetEventFavorites(id) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Could not get user's event favourites.")) } json.NewEncoder(w).Encode(favorites) @@ -220,13 +220,13 @@ func AddEventFavorite(w http.ResponseWriter, r *http.Request) { err := service.AddEventFavorite(id, event_favorite_modification.EventName) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Could not add an event favorite for the current user.")) } favorites, err := service.GetEventFavorites(id) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Could not get updated user event favorites.")) } json.NewEncoder(w).Encode(favorites) @@ -244,13 +244,13 @@ func RemoveEventFavorite(w http.ResponseWriter, r *http.Request) { err := service.RemoveEventFavorite(id, event_favorite_modification.EventName) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Could not remove an event favorite for the current user.")) } favorites, err := service.GetEventFavorites(id) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Could not fetch updated event favourites for the user (post-removal).")) } json.NewEncoder(w).Encode(favorites) @@ -263,7 +263,7 @@ func GetStats(w http.ResponseWriter, r *http.Request) { stats, err := service.GetStats() if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.InternalError(err.Error(), "Could not fetch event service statistics.")) } json.NewEncoder(w).Encode(stats) diff --git a/services/event/service/event_service.go b/services/event/service/event_service.go index d65fc3d1..cedadcb6 100644 --- a/services/event/service/event_service.go +++ b/services/event/service/event_service.go @@ -369,7 +369,7 @@ func AddEventFavorite(id string, event string) error { _, err := GetEvent(event) if err != nil { - return errors.New("Could not find event with the given name") + return errors.New("Could not find event with the given name.") } event_favorites, err := GetEventFavorites(id) diff --git a/services/mail/controller/controller.go b/services/mail/controller/controller.go index a1f56df7..57e10cfd 100644 --- a/services/mail/controller/controller.go +++ b/services/mail/controller/controller.go @@ -33,7 +33,7 @@ func SendMail(w http.ResponseWriter, r *http.Request) { mail_status, err := service.SendMailByID(mail_order) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.InternalError(err.Error(), "Could not send email by ID.")) } json.NewEncoder(w).Encode(mail_status) @@ -50,7 +50,7 @@ func SendMailList(w http.ResponseWriter, r *http.Request) { mail_status, err := service.SendMailByList(mail_order_list) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.InternalError(err.Error(), "Could not send email by list.")) } json.NewEncoder(w).Encode(mail_status) @@ -66,13 +66,13 @@ func CreateMailList(w http.ResponseWriter, r *http.Request) { err := service.CreateMailList(mail_list) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.InternalError(err.Error(), "Could not create the specified mail list.")) } created_list, err := service.GetMailList(mail_list.ID) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Could not get mail list.")) } json.NewEncoder(w).Encode(created_list) @@ -88,13 +88,13 @@ func AddToMailList(w http.ResponseWriter, r *http.Request) { err := service.AddToMailList(mail_list) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Could not add user to mail list.")) } modified_list, err := service.GetMailList(mail_list.ID) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Could not get modified mail list.")) } json.NewEncoder(w).Encode(modified_list) @@ -110,13 +110,13 @@ func RemoveFromMailList(w http.ResponseWriter, r *http.Request) { err := service.RemoveFromMailList(mail_list) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Could not remove user from mailing list.")) } modified_list, err := service.GetMailList(mail_list.ID) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Could not get modified mail list.")) } json.NewEncoder(w).Encode(modified_list) @@ -131,7 +131,7 @@ func GetMailList(w http.ResponseWriter, r *http.Request) { mail_list, err := service.GetMailList(id) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Could not get mail list.")) } json.NewEncoder(w).Encode(mail_list) @@ -144,7 +144,7 @@ func GetAllMailLists(w http.ResponseWriter, r *http.Request) { mail_lists, err := service.GetAllMailLists() if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Could not get all mail lists.")) } json.NewEncoder(w).Encode(mail_lists) diff --git a/services/notifications/.gitignore b/services/notifications/.gitignore new file mode 100644 index 00000000..b9ce8281 --- /dev/null +++ b/services/notifications/.gitignore @@ -0,0 +1 @@ +api-notifications diff --git a/services/notifications/Dockerfile b/services/notifications/Dockerfile new file mode 100755 index 00000000..30ca772b --- /dev/null +++ b/services/notifications/Dockerfile @@ -0,0 +1,14 @@ +FROM ubuntu:12.04 + +EXPOSE 8008 + +WORKDIR /opt/hackillinois/ + +ADD hackillinois-api-notifications /opt/hackillinois/ + +RUN apt-get update +RUN apt-get install -y ca-certificates + +RUN chmod +x hackillinois-api-notifications + +CMD ["./hackillinois-api-notifications"] diff --git a/services/notifications/Makefile b/services/notifications/Makefile new file mode 100644 index 00000000..8e971a04 --- /dev/null +++ b/services/notifications/Makefile @@ -0,0 +1,3 @@ +export BASE_DIR:=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) + +include ../../makefiles/common.mk diff --git a/services/notifications/README.md b/services/notifications/README.md new file mode 100644 index 00000000..795c7bfd --- /dev/null +++ b/services/notifications/README.md @@ -0,0 +1,4 @@ +Notifications +====== + +This is the notifications microservice supporting hackillinois. This service allows notifications to be sent to a user's mobile device. diff --git a/services/notifications/buildspec.yml b/services/notifications/buildspec.yml new file mode 100644 index 00000000..2548109b --- /dev/null +++ b/services/notifications/buildspec.yml @@ -0,0 +1,24 @@ +version: 0.2 + +phases: + pre_build: + commands: + - echo Logging in to Amazon ECR... + - aws --version + - $(aws ecr get-login --region $AWS_DEFAULT_REGION --no-include-email) + - REPOSITORY_URI=461497099412.dkr.ecr.us-east-1.amazonaws.com/hackillinois-api-notifications + build: + commands: + - echo Build started on `date` + - echo Building the Docker image... + - docker build -t hackillinois-api-notifications . + - docker tag hackillinois-api-notifications $REPOSITORY_URI:latest + post_build: + commands: + - echo Build completed on `date` + - echo Pushing the Docker images... + - docker push $REPOSITORY_URI:latest + - echo Writing image definitions file... + - printf '[{"name":"hackillinois-api-notifications","imageUri":"%s"}]' $REPOSITORY_URI:latest > imagedefinitions.json +artifacts: + files: imagedefinitions.json diff --git a/services/notifications/config/config.go b/services/notifications/config/config.go new file mode 100644 index 00000000..e806a761 --- /dev/null +++ b/services/notifications/config/config.go @@ -0,0 +1,70 @@ +package config + +import ( + "github.com/HackIllinois/api/common/configloader" + "os" +) + +var NOTIFICATIONS_DB_HOST string +var NOTIFICATIONS_DB_NAME string + +var NOTIFICATIONS_PORT string + +var IS_PRODUCTION bool + +var SNS_REGION string + +var ANDROID_PLATFORM_ARN string +var IOS_PLATFORM_ARN string + +func init() { + cfg_loader, err := configloader.Load(os.Getenv("HI_CONFIG")) + + if err != nil { + panic(err) + } + + NOTIFICATIONS_DB_HOST, err = cfg_loader.Get("NOTIFICATIONS_DB_HOST") + + if err != nil { + panic(err) + } + + NOTIFICATIONS_DB_NAME, err = cfg_loader.Get("NOTIFICATIONS_DB_NAME") + + if err != nil { + panic(err) + } + + NOTIFICATIONS_PORT, err = cfg_loader.Get("NOTIFICATIONS_PORT") + + if err != nil { + panic(err) + } + + SNS_REGION, err = cfg_loader.Get("SNS_REGION") + + if err != nil { + panic(err) + } + + ANDROID_PLATFORM_ARN, err = cfg_loader.Get("ANDROID_PLATFORM_ARN") + + if err != nil { + panic(err) + } + + IOS_PLATFORM_ARN, err = cfg_loader.Get("IOS_PLATFORM_ARN") + + if err != nil { + panic(err) + } + + production, err := cfg_loader.Get("IS_PRODUCTION") + + if err != nil { + panic(err) + } + + IS_PRODUCTION = (production == "true") +} diff --git a/services/notifications/controller/controller.go b/services/notifications/controller/controller.go new file mode 100644 index 00000000..8c819ed7 --- /dev/null +++ b/services/notifications/controller/controller.go @@ -0,0 +1,221 @@ +package controller + +import ( + "encoding/json" + "github.com/HackIllinois/api/common/errors" + "github.com/HackIllinois/api/services/notifications/models" + "github.com/HackIllinois/api/services/notifications/service" + "github.com/gorilla/mux" + "github.com/justinas/alice" + "net/http" +) + +func SetupController(route *mux.Route) { + router := route.Subrouter() + + router.Handle("/", alice.New().ThenFunc(GetAllTopics)).Methods("GET") + router.Handle("/", alice.New().ThenFunc(CreateTopic)).Methods("POST") + router.Handle("/all/", alice.New().ThenFunc(GetAllNotifications)).Methods("GET") + router.Handle("/device/", alice.New().ThenFunc(RegisterDeviceToUser)).Methods("POST") + router.Handle("/{name}/", alice.New().ThenFunc(GetNotificationsForTopic)).Methods("GET") + router.Handle("/{name}/", alice.New().ThenFunc(DeleteTopic)).Methods("DELETE") + router.Handle("/{name}/", alice.New().ThenFunc(PublishNotification)).Methods("POST") + router.Handle("/{name}/add/", alice.New().ThenFunc(AddUsersToTopic)).Methods("POST") + router.Handle("/{name}/remove/", alice.New().ThenFunc(RemoveUsersFromTopic)).Methods("POST") + router.Handle("/{name}/info/", alice.New().ThenFunc(GetTopicInfo)).Methods("GET") +} + +/* + Endpoint to get all SNS Topics +*/ +func GetAllTopics(w http.ResponseWriter, r *http.Request) { + topics, err := service.GetAllTopics() + + if err != nil { + panic(errors.DatabaseError(err.Error(), "Could not get all SNS topics.")) + } + + json.NewEncoder(w).Encode(topics) +} + +/* + Endpoint to get all past notifications +*/ +func GetAllNotifications(w http.ResponseWriter, r *http.Request) { + notifications_list, err := service.GetAllNotifications() + + if err != nil { + panic(errors.DatabaseError(err.Error(), "Could not get all past notifications.")) + } + + json.NewEncoder(w).Encode(notifications_list) +} + +/* + Endpoint to create a new SNS topic +*/ +func CreateTopic(w http.ResponseWriter, r *http.Request) { + var topic_name models.TopicName + json.NewDecoder(r.Body).Decode(&topic_name) + + err := service.CreateTopic(topic_name.Name) + + if err != nil { + panic(errors.DatabaseError(err.Error(), "Could not create a new SNS topic.")) + } + + json.NewEncoder(w).Encode(topic_name) +} + +/* + Endpoint to delete a SNS topic +*/ +func DeleteTopic(w http.ResponseWriter, r *http.Request) { + topic_name := mux.Vars(r)["name"] + + err := service.DeleteTopic(topic_name) + + if err != nil { + panic(errors.DatabaseError(err.Error(), "Could not delete topic.")) + } + + topics, err := service.GetAllTopics() + + if err != nil { + panic(errors.DatabaseError(err.Error(), "Could not fetch updated topics.")) + } + + json.NewEncoder(w).Encode(topics) +} + +/* + Endpoint to create a new notification +*/ +func PublishNotification(w http.ResponseWriter, r *http.Request) { + topic_name := mux.Vars(r)["name"] + var notification models.Notification + json.NewDecoder(r.Body).Decode(¬ification) + + past_notification, err := service.PublishNotification(topic_name, notification) + + if err != nil { + panic(errors.InternalError(err.Error(), "Could not publish new notification.")) + } + + json.NewEncoder(w).Encode(past_notification) +} + +/* + Endpoint to get all past notifications for a given Topic +*/ +func GetNotificationsForTopic(w http.ResponseWriter, r *http.Request) { + topic_name := mux.Vars(r)["name"] + + notifications_list, err := service.GetNotificationsForTopic(topic_name) + + if err != nil { + panic(errors.DatabaseError(err.Error(), "Could not get all past notifications for the given topic.")) + } + + json.NewEncoder(w).Encode(notifications_list) +} + +/* + Endpoint to get name, ARN for a topic +*/ +func GetTopicInfo(w http.ResponseWriter, r *http.Request) { + topic_name := mux.Vars(r)["name"] + + topic, err := service.GetTopicInfo(topic_name) + + if err != nil { + panic(errors.DatabaseError(err.Error(), "Could not get name / ARN for topic.")) + } + + var topic_public models.TopicPublic + if topic != nil { + topic_public = models.TopicPublic{Name: topic.Name, UserIDs: topic.UserIDs} + } + + json.NewEncoder(w).Encode(topic_public) +} + +/* + Adds users with given userids to the specified topic +*/ +func AddUsersToTopic(w http.ResponseWriter, r *http.Request) { + topic_name := mux.Vars(r)["name"] + + var userid_list models.UserIDList + json.NewDecoder(r.Body).Decode(&userid_list) + + err := service.AddUsersToTopic(topic_name, userid_list) + + if err != nil { + panic(errors.DatabaseError(err.Error(), "Could not add users to specified topic.")) + } + + topic, err := service.GetTopicInfo(topic_name) + + if err != nil { + panic(errors.DatabaseError(err.Error(), "Could not get name / ARN for topic.")) + } + + var topic_public models.TopicPublic + if topic != nil { + topic_public = models.TopicPublic{Name: topic.Name, UserIDs: topic.UserIDs} + } + + json.NewEncoder(w).Encode(topic_public) +} + +/* + Removes users with given userids from the specified topic +*/ +func RemoveUsersFromTopic(w http.ResponseWriter, r *http.Request) { + topic_name := mux.Vars(r)["name"] + + var userid_list models.UserIDList + json.NewDecoder(r.Body).Decode(&userid_list) + + err := service.RemoveUsersFromTopic(topic_name, userid_list) + + if err != nil { + panic(errors.DatabaseError(err.Error(), "Could not remove given users from topic.")) + } + + topic, err := service.GetTopicInfo(topic_name) + + if err != nil { + panic(errors.DatabaseError(err.Error(), "Could not get name / ARN for topic.")) + } + + var topic_public models.TopicPublic + if topic != nil { + topic_public = models.TopicPublic{Name: topic.Name, UserIDs: topic.UserIDs} + } + + json.NewEncoder(w).Encode(topic_public) +} + +/* + Endpoint to register a device token to a given user +*/ +func RegisterDeviceToUser(w http.ResponseWriter, r *http.Request) { + id := r.Header.Get("HackIllinois-Identity") + + if id == "" { + panic(errors.MalformedRequestError("Must provide id to register a device token with.", "Must provide id to register a device token with.")) + } + + var device_registration models.DeviceRegistration + json.NewDecoder(r.Body).Decode(&device_registration) + + err := service.RegisterDeviceToUser(id, device_registration) + + if err != nil { + panic(errors.InternalError(err.Error(), "Could not register device to user.")) + } + + json.NewEncoder(w).Encode(device_registration) +} diff --git a/services/notifications/main.go b/services/notifications/main.go new file mode 100644 index 00000000..92f94803 --- /dev/null +++ b/services/notifications/main.go @@ -0,0 +1,16 @@ +package main + +import ( + "github.com/HackIllinois/api/common/apiserver" + "github.com/HackIllinois/api/services/notifications/config" + "github.com/HackIllinois/api/services/notifications/controller" + "github.com/gorilla/mux" + "log" +) + +func main() { + router := mux.NewRouter() + controller.SetupController(router.PathPrefix("/notifications")) + + log.Fatal(apiserver.StartServer(config.NOTIFICATIONS_PORT, router, "notifications")) +} diff --git a/services/notifications/models/apns_alert.go b/services/notifications/models/apns_alert.go new file mode 100644 index 00000000..96731f94 --- /dev/null +++ b/services/notifications/models/apns_alert.go @@ -0,0 +1,6 @@ +package models + +type APNSAlert struct { + Title string `json:"title"` + Body string `json:"body"` +} diff --git a/services/notifications/models/apns_payload.go b/services/notifications/models/apns_payload.go new file mode 100644 index 00000000..d2a186ca --- /dev/null +++ b/services/notifications/models/apns_payload.go @@ -0,0 +1,5 @@ +package models + +type APNSPayload struct { + Alert APNSAlert `json:"alert"` +} diff --git a/services/notifications/models/device.go b/services/notifications/models/device.go new file mode 100644 index 00000000..fb2bfbbc --- /dev/null +++ b/services/notifications/models/device.go @@ -0,0 +1,9 @@ +package models + +type Device struct { + UserID string `json:"userId"` + DeviceToken string `json:"deviceToken"` + DeviceArn string `json:"deviceArn"` + Platform string `json:"platform"` + Subscriptions map[string]string `json:"subscriptions"` +} diff --git a/services/notifications/models/device_registration.go b/services/notifications/models/device_registration.go new file mode 100644 index 00000000..d9316b3f --- /dev/null +++ b/services/notifications/models/device_registration.go @@ -0,0 +1,6 @@ +package models + +type DeviceRegistration struct { + DeviceToken string `json:"deviceToken"` + Platform string `json:"platform"` +} diff --git a/services/notifications/models/gcm_notification.go b/services/notifications/models/gcm_notification.go new file mode 100644 index 00000000..0600846c --- /dev/null +++ b/services/notifications/models/gcm_notification.go @@ -0,0 +1,6 @@ +package models + +type GCMNotification struct { + Title string `json:"title"` + Body string `json:"body"` +} diff --git a/services/notifications/models/gcm_payload.go b/services/notifications/models/gcm_payload.go new file mode 100644 index 00000000..dcf807c2 --- /dev/null +++ b/services/notifications/models/gcm_payload.go @@ -0,0 +1,5 @@ +package models + +type GCMPayload struct { + Notification GCMNotification `json:"notification"` +} diff --git a/services/notifications/models/notification.go b/services/notifications/models/notification.go new file mode 100644 index 00000000..f2b7e92a --- /dev/null +++ b/services/notifications/models/notification.go @@ -0,0 +1,6 @@ +package models + +type Notification struct { + Body string `json:"body"` + Title string `json:"title"` +} diff --git a/services/notifications/models/notification_list.go b/services/notifications/models/notification_list.go new file mode 100644 index 00000000..97df01a7 --- /dev/null +++ b/services/notifications/models/notification_list.go @@ -0,0 +1,5 @@ +package models + +type NotificationList struct { + Notifications []PastNotification `json:"notifications"` +} diff --git a/services/notifications/models/notification_payload.go b/services/notifications/models/notification_payload.go new file mode 100644 index 00000000..a0e08e32 --- /dev/null +++ b/services/notifications/models/notification_payload.go @@ -0,0 +1,7 @@ +package models + +type NotificationPayload struct { + APNS string `json:"APNS"` + GCM string `json:"GCM"` + Default string `json:"default"` +} diff --git a/services/notifications/models/past_notification.go b/services/notifications/models/past_notification.go new file mode 100644 index 00000000..03fb1f74 --- /dev/null +++ b/services/notifications/models/past_notification.go @@ -0,0 +1,8 @@ +package models + +type PastNotification struct { + TopicName string `json:"topicName"` + Body string `json:"body"` + Title string `json:"title"` + Time int64 `json:"time"` +} diff --git a/services/notifications/models/topic.go b/services/notifications/models/topic.go new file mode 100644 index 00000000..77fac468 --- /dev/null +++ b/services/notifications/models/topic.go @@ -0,0 +1,7 @@ +package models + +type Topic struct { + Name string `json:"name"` + Arn string `json:"arn"` + UserIDs []string `json:"userIds"` +} diff --git a/services/notifications/models/topic_list.go b/services/notifications/models/topic_list.go new file mode 100644 index 00000000..d92278fb --- /dev/null +++ b/services/notifications/models/topic_list.go @@ -0,0 +1,5 @@ +package models + +type TopicList struct { + Topics []string `json:"topics"` +} diff --git a/services/notifications/models/topic_name.go b/services/notifications/models/topic_name.go new file mode 100644 index 00000000..0baa9c24 --- /dev/null +++ b/services/notifications/models/topic_name.go @@ -0,0 +1,5 @@ +package models + +type TopicName struct { + Name string `json:"name"` +} diff --git a/services/notifications/models/topic_public.go b/services/notifications/models/topic_public.go new file mode 100644 index 00000000..8adfb2e1 --- /dev/null +++ b/services/notifications/models/topic_public.go @@ -0,0 +1,6 @@ +package models + +type TopicPublic struct { + Name string `json:"name"` + UserIDs []string `json:"userIds"` +} diff --git a/services/notifications/models/userid_list.go b/services/notifications/models/userid_list.go new file mode 100644 index 00000000..9ebb2c48 --- /dev/null +++ b/services/notifications/models/userid_list.go @@ -0,0 +1,5 @@ +package models + +type UserIDList struct { + UserIDs []string `json:"userIds"` +} diff --git a/services/notifications/service/notifications_service.go b/services/notifications/service/notifications_service.go new file mode 100644 index 00000000..347fd489 --- /dev/null +++ b/services/notifications/service/notifications_service.go @@ -0,0 +1,519 @@ +package service + +import ( + "encoding/json" + "errors" + "fmt" + "github.com/HackIllinois/api/common/database" + "github.com/HackIllinois/api/services/notifications/config" + "github.com/HackIllinois/api/services/notifications/models" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/sns" + "strings" + "time" +) + +const APPLICATION_PROTOCOL = "application" +const MESSAGE_STRUCTURE = "json" + +var sess *session.Session +var client *sns.SNS +var db database.Database + +func init() { + sess = session.Must(session.NewSession(&aws.Config{ + Region: aws.String(config.SNS_REGION), + })) + client = sns.New(sess) + + db_connection, err := database.InitDatabase(config.NOTIFICATIONS_DB_HOST, config.NOTIFICATIONS_DB_NAME) + + if err != nil { + panic(err) + } + + db = db_connection +} + +/* + Returns a list of available SNS Topics +*/ +func GetAllTopics() (*models.TopicList, error) { + var topics []models.Topic + err := db.FindAll("topics", nil, &topics) + + if err != nil { + return nil, err + } + + var topic_list models.TopicList + for _, topic := range topics { + topic_list.Topics = append(topic_list.Topics, topic.Name) + } + + return &topic_list, nil +} + +/* + Returns a list of available SNS Topics +*/ +func GetAllNotifications() (*models.NotificationList, error) { + var notifications []models.PastNotification + + err := db.FindAll("notifications", nil, ¬ifications) + + if err != nil { + return nil, err + } + + notifications_list := models.NotificationList{ + Notifications: notifications, + } + + return ¬ifications_list, nil +} + +/* + Creates an SNS Topic +*/ +func CreateTopic(name string) error { + var arn string + + if config.IS_PRODUCTION { + out, err := client.CreateTopic(&sns.CreateTopicInput{Name: &name}) + + if err != nil { + return err + } + + arn = *out.TopicArn + } + + _, err := GetTopicInfo(name) + + if err != database.ErrNotFound { + if err != nil { + return err + } + return errors.New("Topic already exists") + } + + topic := models.Topic{Arn: arn, Name: name, UserIDs: nil} + + err = db.Insert("topics", &topic) + + if err != nil { + return err + } + + return nil +} + +/* + Deletes an SNS Topic +*/ +func DeleteTopic(name string) error { + + topic, err := GetTopicInfo(name) + + if err != nil { + return err + } + + if config.IS_PRODUCTION { + _, err = client.DeleteTopic(&sns.DeleteTopicInput{TopicArn: &topic.Arn}) + + if err != nil { + return err + } + } + + topic_selector := database.QuerySelector{ + "name": name, + } + + err = db.RemoveOne("topics", topic_selector) + + if err != nil { + return err + } + + return nil +} + +func GetTopicInfo(name string) (*models.Topic, error) { + topic_selector := database.QuerySelector{ + "name": name, + } + + var topic models.Topic + + err := db.FindOne("topics", topic_selector, &topic) + + if err != nil { + return nil, err + } + + return &topic, nil +} + +/* + Dispatches a notification to a given SNS Topic +*/ +func PublishNotification(topic_name string, notification models.Notification) (*models.PastNotification, error) { + + topic, err := GetTopicInfo(topic_name) + + if err != nil { + return nil, err + } + + notification_json_str, err := GenerateNotificationJson(notification) + + if err != nil { + return nil, err + } + + arn := topic.Arn + message_structure := MESSAGE_STRUCTURE + + if config.IS_PRODUCTION { + _, err = client.Publish(&sns.PublishInput{ + TopicArn: &arn, + Message: notification_json_str, + MessageStructure: &message_structure, + }) + + if err != nil { + return nil, err + } + } + + current_time := time.Now().Unix() + + past_notification := models.PastNotification{TopicName: topic_name, Title: notification.Title, Body: notification.Body, Time: current_time} + err = db.Insert("notifications", &past_notification) + + return &past_notification, nil +} + +func GetNotificationsForTopic(topic_name string) (*models.NotificationList, error) { + topic_name_selector := database.QuerySelector{ + "topicname": topic_name, + } + + var notifications []models.PastNotification + + err := db.FindAll("notifications", topic_name_selector, ¬ifications) + + if err != nil { + return nil, err + } + + notifications_list := models.NotificationList{ + Notifications: notifications, + } + + return ¬ifications_list, nil +} + +/* + Adds the given userids to the specified topic +*/ +func AddUsersToTopic(topic_name string, userid_list models.UserIDList) error { + selector := database.QuerySelector{ + "name": topic_name, + } + + modifier := database.QuerySelector{ + "$addToSet": database.QuerySelector{ + "userids": database.QuerySelector{ + "$each": userid_list.UserIDs, + }, + }, + } + + topic_selector := database.QuerySelector{ + "name": topic_name, + } + + var topic models.Topic + err := db.FindOne("topics", topic_selector, &topic) + + if err != nil { + return err + } + + // Subscribe each of the specified users' devices to this topic + query := database.QuerySelector{ + "userid": database.QuerySelector{ + "$in": userid_list.UserIDs, + }, + } + + var devices []models.Device + err = db.FindAll("devices", query, &devices) + + if err != nil { + return err + } + + for _, device := range devices { + err := SubscribeDeviceToTopic(topic, device) + + if err != nil { + return err + } + } + + return db.Update("topics", selector, &modifier) +} + +/* + Removes the given userids from the specified topic +*/ +func RemoveUsersFromTopic(topic_name string, userid_list models.UserIDList) error { + selector := database.QuerySelector{ + "name": topic_name, + } + + modifier := database.QuerySelector{ + "$pull": database.QuerySelector{ + "userids": database.QuerySelector{ + "$in": userid_list.UserIDs, + }, + }, + } + + topic_selector := database.QuerySelector{ + "name": topic_name, + } + + var topic models.Topic + err := db.FindOne("topics", topic_selector, &topic) + + if err != nil { + return err + } + + // Unsubscribe each of the specificed users' devices from this topic + query := database.QuerySelector{ + "userid": database.QuerySelector{ + "$in": userid_list.UserIDs, + }, + } + + var devices []models.Device + err = db.FindAll("devices", query, &devices) + + if err != nil { + return err + } + + for _, device := range devices { + err = UnsubscribeDeviceFromTopic(topic, device) + + if err != nil { + return err + } + } + + return db.Update("topics", selector, &modifier) +} + +/* + Links the given device token with a user +*/ +func RegisterDeviceToUser(user_id string, device_reg models.DeviceRegistration) error { + var device_arn string + var platform_arn string + + // Map platform (android, ios etc) to its ARN + switch strings.ToLower(device_reg.Platform) { + case "android": + platform_arn = config.ANDROID_PLATFORM_ARN + case "ios": + platform_arn = config.IOS_PLATFORM_ARN + default: + return errors.New("Invalid platform") + } + + if config.IS_PRODUCTION { + out, err := client.CreatePlatformEndpoint(&sns.CreatePlatformEndpointInput{CustomUserData: &user_id, Token: &device_reg.DeviceToken, PlatformApplicationArn: &platform_arn}) + + if err != nil { + return err + } + + device_arn = *out.EndpointArn + } + + subs := make(map[string]string) + device := models.Device{DeviceArn: device_arn, DeviceToken: device_reg.DeviceToken, Platform: device_reg.Platform, UserID: user_id, Subscriptions: subs} + + err := db.Insert("devices", device) + + if err != nil { + return err + } + + // Subscribe the device to all of a user's topics + + topic_selector := database.QuerySelector{ + "userids": database.QuerySelector{ + "$all": [1]string{user_id}, + }, + } + + var topics []models.Topic + err = db.FindAll("topics", topic_selector, &topics) + + if err != nil { + return err + } + + for _, topic := range topics { + err = SubscribeDeviceToTopic(topic, device) + + if err != nil { + return err + } + } + + return nil +} + +/* + Subscribes a given Device to a Topic, in the database and SNS +*/ +func SubscribeDeviceToTopic(topic models.Topic, device models.Device) error { + app_protocol := APPLICATION_PROTOCOL + + var sub_arn string + + if config.IS_PRODUCTION { + out, err := client.Subscribe(&sns.SubscribeInput{Protocol: &app_protocol, TopicArn: &topic.Arn, Endpoint: &device.DeviceArn}) + + if err != nil { + return err + } + + sub_arn = *out.SubscriptionArn + } + + device_selector := database.QuerySelector{ + "devicearn": device.DeviceArn, + } + + set_query := fmt.Sprintf("subscriptions.%s", topic.Name) + + // Keep track of subscription's ARN so we can unsubscribe later + device_modifier := database.QuerySelector{ + "$set": database.QuerySelector{ + set_query: sub_arn, + }, + } + + err := db.Update("devices", device_selector, &device_modifier) + + return err +} + +/* + Unsubscribes a given Device from a Topic, both in the database and SNS +*/ +func UnsubscribeDeviceFromTopic(topic models.Topic, device models.Device) error { + sub_arn, ok := device.Subscriptions[topic.Name] + + if !ok { + return errors.New("Device not subscribed to topic") + } + + if config.IS_PRODUCTION { + _, err := client.Unsubscribe(&sns.UnsubscribeInput{SubscriptionArn: &sub_arn}) + + if err != nil { + return err + } + } + + device_selector := database.QuerySelector{ + "devicearn": device.DeviceArn, + } + + set_query := fmt.Sprintf("subscriptions.%s", topic.Name) + + // Unset device's subscription ARN for this topic since it's no longer needed + device_modifier := database.QuerySelector{ + "$unset": database.QuerySelector{ + set_query: "", + }, + } + + err := db.Update("devices", device_selector, &device_modifier) + + if err != nil { + return err + } + + return nil +} + +/* + Returns a list of registered devices +*/ +func GetAllDevices() (*[]models.Device, error) { + var devices []models.Device + err := db.FindAll("devices", nil, &devices) + + if err != nil { + return nil, err + } + + return &devices, nil +} + +func GenerateNotificationJson(notification models.Notification) (*string, error) { + apns_payload := models.APNSPayload{ + Alert: models.APNSAlert{ + Title: notification.Title, + Body: notification.Body, + }, + } + + gcm_payload := models.GCMPayload{ + Notification: models.GCMNotification{ + Title: notification.Title, + Body: notification.Body, + }, + } + + apns_payload_json, err := json.Marshal(apns_payload) + + if err != nil { + return nil, err + } + + gcm_payload_json, err := json.Marshal(gcm_payload) + + if err != nil { + return nil, err + } + + notification_payload := models.NotificationPayload{ + APNS: string(apns_payload_json), + GCM: string(gcm_payload_json), + Default: notification.Body, + } + + notification_json, err := json.Marshal(notification_payload) + + if err != nil { + return nil, err + } + + notification_json_str := string(notification_json) + + return ¬ification_json_str, nil +} diff --git a/services/notifications/tests/notifications_test.go b/services/notifications/tests/notifications_test.go new file mode 100644 index 00000000..ed9d91c5 --- /dev/null +++ b/services/notifications/tests/notifications_test.go @@ -0,0 +1,568 @@ +package tests + +import ( + "github.com/HackIllinois/api/common/database" + "github.com/HackIllinois/api/services/notifications/config" + "github.com/HackIllinois/api/services/notifications/models" + "github.com/HackIllinois/api/services/notifications/service" + "reflect" + "testing" +) + +var db database.Database + +func init() { + db_connection, err := database.InitDatabase(config.NOTIFICATIONS_DB_HOST, config.NOTIFICATIONS_DB_NAME) + + if err != nil { + panic(err) + } + + db = db_connection +} + +/* + Initialize db with a test topic and notification +*/ +func SetupTestDB(t *testing.T) { + topic := models.Topic{ + Name: "test_topic", + Arn: "arn:test", + UserIDs: []string{"test_user"}, + } + + err := db.Insert("topics", &topic) + + if err != nil { + t.Fatal(err) + } + + notification := models.PastNotification{ + Body: "test message", + Title: "test title", + TopicName: "test_topic", + Time: 2000, + } + + err = db.Insert("notifications", ¬ification) + + if err != nil { + t.Fatal(err) + } + + device := models.Device{ + UserID: "test_user", + DeviceToken: "token1", + DeviceArn: "arn:device_test_1", + Platform: "android", + Subscriptions: map[string]string{"test_topic": ""}, + } + device2 := models.Device{ + UserID: "test_user_2", + DeviceToken: "token2", + DeviceArn: "arn:device_test_2", + Platform: "android", + Subscriptions: map[string]string{}, + } + + err = db.Insert("devices", &device) + err = db.Insert("devices", &device2) + + if err != nil { + t.Fatal(err) + } +} + +/* + Drop test db +*/ +func CleanupTestDB(t *testing.T) { + err := db.DropDatabase() + + if err != nil { + t.Fatal(err) + } +} + +/* + Service level test for getting all topics from db +*/ +func TestGetAllTopicsSerice(t *testing.T) { + SetupTestDB(t) + + topic := models.Topic{ + Name: "test_topic_2", + Arn: "arn:test2", + UserIDs: []string{"test_user_2"}, + } + + err := db.Insert("topics", &topic) + + if err != nil { + t.Fatal(err) + } + + actual_topic_list, err := service.GetAllTopics() + + if err != nil { + t.Fatal(err) + } + + expected_topic_list := models.TopicList{ + Topics: []string{ + "test_topic", + "test_topic_2", + }, + } + + if !reflect.DeepEqual(actual_topic_list, &expected_topic_list) { + t.Errorf("Wrong topic list. Expected %v, got %v", &expected_topic_list, actual_topic_list) + } + + CleanupTestDB(t) +} + +/* + Service level test for creating a notification topic +*/ +func TestCreateTopicService(t *testing.T) { + SetupTestDB(t) + + err := service.CreateTopic("test_topic_2") + + if err != nil { + t.Fatal(err) + } + + actual_topic_list, err := service.GetAllTopics() + + if err != nil { + t.Fatal(err) + } + + expected_topic_list := models.TopicList{ + Topics: []string{ + "test_topic", + "test_topic_2", + }, + } + + if !reflect.DeepEqual(actual_topic_list, &expected_topic_list) { + t.Errorf("Wrong topic list. Expected %v, got %v", &expected_topic_list, actual_topic_list) + } + + CleanupTestDB(t) +} + +/* + Service level test for getting all notifications from db +*/ +func TestGetAllNotificationsService(t *testing.T) { + SetupTestDB(t) + + notification := models.PastNotification{ + Body: "test message 2", + Title: "test title 2", + TopicName: "test_topic_2", + Time: 3000, + } + + err := db.Insert("notifications", ¬ification) + + if err != nil { + t.Fatal(err) + } + + actual_notification_list, err := service.GetAllNotifications() + + if err != nil { + t.Fatal(err) + } + + expected_notification_list := models.NotificationList{ + Notifications: []models.PastNotification{ + models.PastNotification{ + Body: "test message", + Title: "test title", + TopicName: "test_topic", + Time: 2000, + }, + models.PastNotification{ + Body: "test message 2", + Title: "test title 2", + TopicName: "test_topic_2", + Time: 3000, + }, + }, + } + + if !reflect.DeepEqual(actual_notification_list, &expected_notification_list) { + t.Errorf("Wrong notification list. Expected %v, got %v", &expected_notification_list, actual_notification_list) + } + + CleanupTestDB(t) +} + +/* + Service level test for getting notifications for a specific topic from db +*/ +func TestGetNotificationsForTopicService(t *testing.T) { + SetupTestDB(t) + + notification := models.PastNotification{ + Body: "test message again", + Title: "test title again", + TopicName: "test_topic", + Time: 5000, + } + + err := db.Insert("notifications", ¬ification) + + if err != nil { + t.Fatal(err) + } + + notification = models.PastNotification{ + Body: "test message 2", + Title: "test title 2", + TopicName: "test_topic_2", + Time: 3000, + } + + err = db.Insert("notifications", ¬ification) + + if err != nil { + t.Fatal(err) + } + + actual_notification_list, err := service.GetNotificationsForTopic("test_topic") + + if err != nil { + t.Fatal(err) + } + + expected_notification_list := models.NotificationList{ + Notifications: []models.PastNotification{ + models.PastNotification{ + Body: "test message", + Title: "test title", + TopicName: "test_topic", + Time: 2000, + }, + models.PastNotification{ + Body: "test message again", + Title: "test title again", + TopicName: "test_topic", + Time: 5000, + }, + }, + } + + if !reflect.DeepEqual(actual_notification_list, &expected_notification_list) { + t.Errorf("Wrong notification list. Expected %v, got %v", &expected_notification_list, actual_notification_list) + } + + CleanupTestDB(t) +} + +/* + Service level test for deleting a topic from db +*/ +func TestDeleteTopicService(t *testing.T) { + SetupTestDB(t) + + topic := models.Topic{ + Name: "test_topic_2", + Arn: "arn:test2", + } + + err := db.Insert("topics", &topic) + + if err != nil { + t.Fatal(err) + } + + err = service.DeleteTopic("test_topic_2") + + if err != nil { + t.Fatal(err) + } + + actual_topic_list, err := service.GetAllTopics() + + if err != nil { + t.Fatal(err) + } + + expected_topic_list := models.TopicList{ + Topics: []string{ + "test_topic", + }, + } + + if !reflect.DeepEqual(actual_topic_list, &expected_topic_list) { + t.Errorf("Wrong topic list. Expected %v, got %v", &expected_topic_list, actual_topic_list) + } + + CleanupTestDB(t) +} + +/* + Service level test for creating a notification +*/ +func TestCreateNotificationService(t *testing.T) { + SetupTestDB(t) + + notification := models.Notification{ + Body: "test message 2", + Title: "test title 2", + } + + past_notification, err := service.PublishNotification("test_topic", notification) + + if err != nil { + t.Fatal(err) + } + + actual_notification_list, err := service.GetNotificationsForTopic("test_topic") + + if err != nil { + t.Fatal(err) + } + + expected_notification_list := models.NotificationList{ + Notifications: []models.PastNotification{ + models.PastNotification{ + Body: "test message", + Title: "test title", + TopicName: "test_topic", + Time: 2000, + }, + models.PastNotification{ + Body: "test message 2", + Title: "test title 2", + TopicName: "test_topic", + Time: past_notification.Time, + }, + }, + } + + if !reflect.DeepEqual(actual_notification_list, &expected_notification_list) { + t.Errorf("Wrong notification list. Expected %v, got %v", &expected_notification_list, actual_notification_list) + } + + CleanupTestDB(t) +} + +/* + Service level test for getting topic info from db +*/ +func TestGetTopicInfoService(t *testing.T) { + SetupTestDB(t) + + actual_topic_info, err := service.GetTopicInfo("test_topic") + + if err != nil { + t.Fatal(err) + } + + expected_topic_info := models.Topic{ + Name: "test_topic", + Arn: "arn:test", + UserIDs: []string{"test_user"}, + } + + if !reflect.DeepEqual(actual_topic_info, &expected_topic_info) { + t.Errorf("Wrong topic info. Expected %v, got %v", &expected_topic_info, actual_topic_info) + } + + CleanupTestDB(t) +} + +/* + Service level test for subscribing users to a topic +*/ +func TestSubscribeUserService(t *testing.T) { + SetupTestDB(t) + + userid_list := models.UserIDList{ + UserIDs: []string{ + "test_user_2", + "test_user_3", + }, + } + + err := service.AddUsersToTopic("test_topic", userid_list) + + if err != nil { + t.Fatal(err) + } + + actual_topic_info, err := service.GetTopicInfo("test_topic") + + if err != nil { + t.Fatal(err) + } + + expected_topic_info := models.Topic{ + Name: "test_topic", + Arn: "arn:test", + UserIDs: []string{ + "test_user", + "test_user_2", + "test_user_3", + }, + } + + if !reflect.DeepEqual(actual_topic_info, &expected_topic_info) { + t.Errorf("Wrong topic info. Expected %v, got %v", &expected_topic_info, actual_topic_info) + } + + actual_devices_list, err := service.GetAllDevices() + + if err != nil { + t.Fatal(err) + } + + expected_devices_list := []models.Device{ + models.Device{ + UserID: "test_user", + DeviceToken: "token1", + DeviceArn: "arn:device_test_1", + Platform: "android", + Subscriptions: map[string]string{"test_topic": ""}, + }, + models.Device{ + UserID: "test_user_2", + DeviceToken: "token2", + DeviceArn: "arn:device_test_2", + Platform: "android", + Subscriptions: map[string]string{"test_topic": ""}, + }, + } + + if !reflect.DeepEqual(actual_devices_list, &expected_devices_list) { + t.Errorf("Wrong devices list. Expected %v, got %v", &expected_devices_list, actual_devices_list) + } + + CleanupTestDB(t) +} + +/* + Service level test for unsubscribing users from a topic +*/ +func TestUnsubscribeUserService(t *testing.T) { + SetupTestDB(t) + + userid_list := models.UserIDList{ + UserIDs: []string{ + "test_user", + "test_user_3", + }, + } + + err := service.RemoveUsersFromTopic("test_topic", userid_list) + + if err != nil { + t.Fatal(err) + } + + actual_topic_info, err := service.GetTopicInfo("test_topic") + + if err != nil { + t.Fatal(err) + } + + expected_topic_info := models.Topic{ + Name: "test_topic", + Arn: "arn:test", + UserIDs: []string{}, + } + + if !reflect.DeepEqual(actual_topic_info, &expected_topic_info) { + t.Errorf("Wrong topic info. Expected %v, got %v", &expected_topic_info, actual_topic_info) + } + + actual_devices_list, err := service.GetAllDevices() + + if err != nil { + t.Fatal(err) + } + + expected_devices_list := []models.Device{ + models.Device{ + UserID: "test_user", + DeviceToken: "token1", + DeviceArn: "arn:device_test_1", + Platform: "android", + Subscriptions: map[string]string{}, + }, + models.Device{ + UserID: "test_user_2", + DeviceToken: "token2", + DeviceArn: "arn:device_test_2", + Platform: "android", + Subscriptions: map[string]string{}, + }, + } + + if !reflect.DeepEqual(actual_devices_list, &expected_devices_list) { + t.Errorf("Wrong devices list. Expected %v, got %v", &expected_devices_list, actual_devices_list) + } + + CleanupTestDB(t) +} + +/* + Service level test for registering a device to a user +*/ +func TestRegisterDevice(t *testing.T) { + SetupTestDB(t) + + device_registration := models.DeviceRegistration{ + DeviceToken: "token3", + Platform: "android", + } + + err := service.RegisterDeviceToUser("test_user", device_registration) + + if err != nil { + t.Fatal(err) + } + + actual_devices_list, err := service.GetAllDevices() + + if err != nil { + t.Fatal(err) + } + + expected_devices_list := []models.Device{ + models.Device{ + UserID: "test_user", + DeviceToken: "token1", + DeviceArn: "arn:device_test_1", + Platform: "android", + Subscriptions: map[string]string{"test_topic": ""}, + }, + models.Device{ + UserID: "test_user_2", + DeviceToken: "token2", + DeviceArn: "arn:device_test_2", + Platform: "android", + Subscriptions: map[string]string{}, + }, + models.Device{ + UserID: "test_user", + DeviceToken: "token3", + DeviceArn: "", + Platform: "android", + Subscriptions: map[string]string{"test_topic": ""}, + }, + } + + if !reflect.DeepEqual(actual_devices_list, &expected_devices_list) { + t.Errorf("Wrong devices list. Expected %v, got %v", &expected_devices_list, actual_devices_list) + } + + CleanupTestDB(t) +} diff --git a/services/registration/controller/controller.go b/services/registration/controller/controller.go index 16243581..14d9b540 100644 --- a/services/registration/controller/controller.go +++ b/services/registration/controller/controller.go @@ -81,7 +81,7 @@ func GetCurrentUserRegistration(w http.ResponseWriter, r *http.Request) { user_registration, err := service.GetUserRegistration(id) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Could not get current user's registration.")) } json.NewEncoder(w).Encode(user_registration) @@ -95,14 +95,14 @@ func CreateCurrentUserRegistration(w http.ResponseWriter, r *http.Request) { id := r.Header.Get("HackIllinois-Identity") if id == "" { - panic(errors.UnprocessableError("Must provide id")) + panic(errors.MalformedRequestError("Must provide id in request.", "Must provide id in request.")) } user_registration := datastore.NewDataStore(config.REGISTRATION_DEFINITION) err := json.NewDecoder(r.Body).Decode(&user_registration) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.InternalError(err.Error(), "Could not decode user registration information. Possible failure in JSON validation, or invalid registration format.")) } user_registration.Data["id"] = id @@ -110,7 +110,7 @@ func CreateCurrentUserRegistration(w http.ResponseWriter, r *http.Request) { user_info, err := service.GetUserInfo(id) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.InternalError(err.Error(), "Could not get user info.")) } user_registration.Data["github"] = user_info.Username @@ -124,25 +124,25 @@ func CreateCurrentUserRegistration(w http.ResponseWriter, r *http.Request) { err = service.CreateUserRegistration(id, user_registration) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.InternalError(err.Error(), "Could not create user registration.")) } err = service.AddApplicantRole(id) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.InternalError(err.Error(), "Could not add applicant role.")) } err = service.AddInitialDecision(id) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.InternalError(err.Error(), "Could not add initial decision.")) } updated_registration, err := service.GetUserRegistration(id) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Could not get user registration.")) } // Add user to mailing list @@ -150,7 +150,7 @@ func CreateCurrentUserRegistration(w http.ResponseWriter, r *http.Request) { err = service.AddUserToMailList(id, mail_list) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.InternalError(err.Error(), "Could not add user to registered users mailing list.")) } // Send confirmation mail @@ -158,7 +158,7 @@ func CreateCurrentUserRegistration(w http.ResponseWriter, r *http.Request) { err = service.SendUserMail(id, mail_template) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.InternalError(err.Error(), "Could not send registration confirmation email.")) } json.NewEncoder(w).Encode(updated_registration) @@ -172,14 +172,14 @@ func UpdateCurrentUserRegistration(w http.ResponseWriter, r *http.Request) { id := r.Header.Get("HackIllinois-Identity") if id == "" { - panic(errors.UnprocessableError("Must provide id")) + panic(errors.MalformedRequestError("Must provide id in request.", "Must provide id in request.")) } user_registration := datastore.NewDataStore(config.REGISTRATION_DEFINITION) err := json.NewDecoder(r.Body).Decode(&user_registration) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.InternalError(err.Error(), "Could not decode user registration information. Possible failure in JSON validation, or invalid registration format.")) } user_registration.Data["id"] = id @@ -187,13 +187,13 @@ func UpdateCurrentUserRegistration(w http.ResponseWriter, r *http.Request) { user_info, err := service.GetUserInfo(id) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.InternalError(err.Error(), "Could not get user info.")) } original_registration, err := service.GetUserRegistration(id) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Could not get user's original registration.")) } user_registration.Data["github"] = user_info.Username @@ -207,20 +207,20 @@ func UpdateCurrentUserRegistration(w http.ResponseWriter, r *http.Request) { err = service.UpdateUserRegistration(id, user_registration) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.InternalError(err.Error(), "Could not update user's registration.")) } updated_registration, err := service.GetUserRegistration(id) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Could not fetch user's updated registration.")) } mail_template := "registration_update" err = service.SendUserMail(id, mail_template) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.InternalError(err.Error(), "Could not send registration update email.")) } json.NewEncoder(w).Encode(updated_registration) @@ -234,7 +234,7 @@ func GetFilteredUserRegistrations(w http.ResponseWriter, r *http.Request) { user_registrations, err := service.GetFilteredUserRegistrations(parameters) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Could not get filtered user registrations.")) } json.NewEncoder(w).Encode(user_registrations) @@ -249,7 +249,7 @@ func GetCurrentMentorRegistration(w http.ResponseWriter, r *http.Request) { mentor_registration, err := service.GetMentorRegistration(id) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Could not get mentor registration.")) } json.NewEncoder(w).Encode(mentor_registration) @@ -262,14 +262,14 @@ func CreateCurrentMentorRegistration(w http.ResponseWriter, r *http.Request) { id := r.Header.Get("HackIllinois-Identity") if id == "" { - panic(errors.UnprocessableError("Must provide id")) + panic(errors.MalformedRequestError("Must provide id in request.", "Must provide id in request.")) } mentor_registration := datastore.NewDataStore(config.MENTOR_REGISTRATION_DEFINITION) err := json.NewDecoder(r.Body).Decode(&mentor_registration) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.InternalError(err.Error(), "Could not decode mentor registration information. Possible failure in JSON validation, or invalid registration format.")) } mentor_registration.Data["id"] = id @@ -277,7 +277,7 @@ func CreateCurrentMentorRegistration(w http.ResponseWriter, r *http.Request) { user_info, err := service.GetUserInfo(id) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Could not get mentor's user info.")) } mentor_registration.Data["github"] = user_info.Username @@ -291,19 +291,19 @@ func CreateCurrentMentorRegistration(w http.ResponseWriter, r *http.Request) { err = service.CreateMentorRegistration(id, mentor_registration) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.InternalError(err.Error(), "Could not create mentor registration.")) } err = service.AddMentorRole(id) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.InternalError(err.Error(), "Could not add mentor role.")) } updated_registration, err := service.GetMentorRegistration(id) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Could not get updated mentor registration.")) } json.NewEncoder(w).Encode(updated_registration) @@ -316,14 +316,14 @@ func UpdateCurrentMentorRegistration(w http.ResponseWriter, r *http.Request) { id := r.Header.Get("HackIllinois-Identity") if id == "" { - panic(errors.UnprocessableError("Must provide id")) + panic(errors.MalformedRequestError("Must provide id in request.", "Must provide id in request.")) } mentor_registration := datastore.NewDataStore(config.MENTOR_REGISTRATION_DEFINITION) err := json.NewDecoder(r.Body).Decode(&mentor_registration) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.InternalError(err.Error(), "Could not decode mentor registration information. Possible failure in JSON validation, or invalid registration format.")) } mentor_registration.Data["id"] = id @@ -331,13 +331,13 @@ func UpdateCurrentMentorRegistration(w http.ResponseWriter, r *http.Request) { user_info, err := service.GetUserInfo(id) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Could not get mentor's user info.")) } original_registration, err := service.GetMentorRegistration(id) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Could not get mentor registration.")) } mentor_registration.Data["github"] = user_info.Username @@ -351,13 +351,13 @@ func UpdateCurrentMentorRegistration(w http.ResponseWriter, r *http.Request) { err = service.UpdateMentorRegistration(id, mentor_registration) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.InternalError(err.Error(), "Could not update mentor registration.")) } updated_registration, err := service.GetMentorRegistration(id) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Could not get updated mentor registration.")) } json.NewEncoder(w).Encode(updated_registration) @@ -372,7 +372,7 @@ func GetUserRegistration(w http.ResponseWriter, r *http.Request) { user_registration, err := service.GetUserRegistration(id) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Could not get user registration.")) } json.NewEncoder(w).Encode(user_registration) @@ -387,7 +387,7 @@ func GetMentorRegistration(w http.ResponseWriter, r *http.Request) { mentor_registration, err := service.GetMentorRegistration(id) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Could not get mentor registration.")) } json.NewEncoder(w).Encode(mentor_registration) @@ -400,7 +400,7 @@ func GetStats(w http.ResponseWriter, r *http.Request) { stats, err := service.GetStats() if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.InternalError(err.Error(), "Could not fetch registration service statistics.")) } json.NewEncoder(w).Encode(stats) diff --git a/services/registration/service/auth_service.go b/services/registration/service/auth_service.go index d2494799..26439b96 100644 --- a/services/registration/service/auth_service.go +++ b/services/registration/service/auth_service.go @@ -35,7 +35,7 @@ func AddRole(id string, role models.Role) error { } if status != http.StatusOK { - return errors.New("Auth service failed to update roles") + return errors.New("Auth service failed to update roles.") } return nil diff --git a/services/registration/service/decision_service.go b/services/registration/service/decision_service.go index 055fdd89..8bf073f0 100644 --- a/services/registration/service/decision_service.go +++ b/services/registration/service/decision_service.go @@ -37,7 +37,7 @@ func AddInitialDecision(id string) error { } if status != http.StatusOK { - return errors.New("Decision service failed to create decision") + return errors.New("Decision service failed to create decision.") } return nil diff --git a/services/registration/service/registration_service.go b/services/registration/service/registration_service.go index c41cfa96..e540b97e 100644 --- a/services/registration/service/registration_service.go +++ b/services/registration/service/registration_service.go @@ -60,7 +60,7 @@ func CreateUserRegistration(id string, user_registration models.UserRegistration if err != nil { return err } - return errors.New("Registration already exists") + return errors.New("Registration already exists.") } err = db.Insert("attendees", &user_registration) diff --git a/services/rsvp/controller/controller.go b/services/rsvp/controller/controller.go index f10e8966..df75b3df 100644 --- a/services/rsvp/controller/controller.go +++ b/services/rsvp/controller/controller.go @@ -30,7 +30,7 @@ func GetUserRsvp(w http.ResponseWriter, r *http.Request) { rsvp, err := service.GetUserRsvp(id) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Cannot get user's RSVP status.")) } json.NewEncoder(w).Encode(rsvp) @@ -45,7 +45,7 @@ func GetCurrentUserRsvp(w http.ResponseWriter, r *http.Request) { rsvp, err := service.GetUserRsvp(id) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Cannot get user's RSVP status.")) } json.NewEncoder(w).Encode(rsvp) @@ -59,21 +59,21 @@ func CreateCurrentUserRsvp(w http.ResponseWriter, r *http.Request) { id := r.Header.Get("HackIllinois-Identity") if id == "" { - panic(errors.UnprocessableError("Must provide id")) + panic(errors.MalformedRequestError("Must provide id in request.", "Must provide id in request.")) } isAccepted, isActive, err := service.IsApplicantAcceptedAndActive(id) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.InternalError(err.Error(), "Could not determine status of applicant decision, which is needed to create an RSVP for the user.")) } if !isAccepted { - panic(errors.UnprocessableError("Applicant must be accepted to rsvp")) + panic(errors.AttributeMismatchError("Applicant not accepted.", "Applicant must be accepted to RSVP.")) } if !isActive { - panic(errors.UnprocessableError("Applicant decision has expired")) + panic(errors.AttributeMismatchError("Applicant decision has expired.", "Applicant decision has expired.")) } var rsvp models.UserRsvp @@ -84,28 +84,28 @@ func CreateCurrentUserRsvp(w http.ResponseWriter, r *http.Request) { err = service.CreateUserRsvp(id, rsvp) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.InternalError(err.Error(), "Could not create an RSVP for the user.")) } if rsvp.IsAttending { err = service.AddAttendeeRole(id) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.AuthorizationError(err.Error(), "Could not add Attendee role to applicant.")) } } updated_rsvp, err := service.GetUserRsvp(id) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Could not get user's RSVP.")) } mail_template := "rsvp_confirmation" err = service.SendUserMail(id, mail_template) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.InternalError(err.Error(), "Could not send user RSVP confirmation mail.")) } json.NewEncoder(w).Encode(updated_rsvp) @@ -119,27 +119,27 @@ func UpdateCurrentUserRsvp(w http.ResponseWriter, r *http.Request) { id := r.Header.Get("HackIllinois-Identity") if id == "" { - panic(errors.UnprocessableError("Must provide id")) + panic(errors.MalformedRequestError("Must provide id in request.", "Must provide id in the request.")) } isAccepted, isActive, err := service.IsApplicantAcceptedAndActive(id) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.InternalError(err.Error(), "Could not determine if applicant was accepted and/or decision expiration status.")) } if !isAccepted { - panic(errors.UnprocessableError("Applicant must be accepted to modify rsvp")) + panic(errors.AttributeMismatchError("Applicant must be accepted to modify RSVP.", "Applicant must be accepted to modify RSVP.")) } if !isActive { - panic(errors.UnprocessableError("Cannot modify rsvp, applicant decision has expired")) + panic(errors.AttributeMismatchError("Cannot modify RSVP, applicant decision has expired.", "Cannot modify RSVP, applicant decision has expired.")) } original_rsvp, err := service.GetUserRsvp(id) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Could not get user's RSVP status.")) } var rsvp models.UserRsvp @@ -150,34 +150,34 @@ func UpdateCurrentUserRsvp(w http.ResponseWriter, r *http.Request) { err = service.UpdateUserRsvp(id, rsvp) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Could not update user RSVP.")) } if !original_rsvp.IsAttending && rsvp.IsAttending { err = service.AddAttendeeRole(id) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.AuthorizationError(err.Error(), "Could not add Attendee role to user.")) } } else if original_rsvp.IsAttending && !rsvp.IsAttending { err = service.RemoveAttendeeRole(id) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.InternalError(err.Error(), "Could not remove Attendee role from user.")) } } updated_rsvp, err := service.GetUserRsvp(id) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Could not get updated RSVP for user.")) } mail_template := "rsvp_update" err = service.SendUserMail(id, mail_template) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.InternalError(err.Error(), "Could not send user confirmation mail for RSVP update.")) } json.NewEncoder(w).Encode(updated_rsvp) @@ -190,7 +190,7 @@ func GetStats(w http.ResponseWriter, r *http.Request) { stats, err := service.GetStats() if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.InternalError(err.Error(), "Could not get RSVP service statistics.")) } json.NewEncoder(w).Encode(stats) diff --git a/services/rsvp/service/auth_service.go b/services/rsvp/service/auth_service.go index d2122ba8..0b396785 100644 --- a/services/rsvp/service/auth_service.go +++ b/services/rsvp/service/auth_service.go @@ -21,7 +21,7 @@ func AddAttendeeRole(id string) error { } if status != http.StatusOK { - return errors.New("Auth service failed to update roles") + return errors.New("Auth service failed to update roles.") } return nil diff --git a/services/rsvp/service/decision_service.go b/services/rsvp/service/decision_service.go index 7e7ca89d..c7e975b5 100644 --- a/services/rsvp/service/decision_service.go +++ b/services/rsvp/service/decision_service.go @@ -21,7 +21,7 @@ func IsApplicantAcceptedAndActive(id string) (bool, bool, error) { } if status != http.StatusOK { - return false, false, errors.New("Decision service failed to return status") + return false, false, errors.New("Decision service failed to return status.") } return decision.Status == "ACCEPTED" && decision.Finalized, time.Now().Unix() < decision.Timestamp+HoursToUnixSeconds(config.DECISION_EXPIRATION_HOURS), nil diff --git a/services/rsvp/service/rsvp_service.go b/services/rsvp/service/rsvp_service.go index 8210a456..2313681a 100644 --- a/services/rsvp/service/rsvp_service.go +++ b/services/rsvp/service/rsvp_service.go @@ -47,7 +47,7 @@ func CreateUserRsvp(id string, rsvp models.UserRsvp) error { if err != nil { return err } - return errors.New("Rsvp already exists") + return errors.New("RSVP already exists.") } err = db.Insert("rsvps", &rsvp) diff --git a/services/stat/controller/controller.go b/services/stat/controller/controller.go index 000372e0..1c674746 100644 --- a/services/stat/controller/controller.go +++ b/services/stat/controller/controller.go @@ -26,7 +26,7 @@ func GetStat(w http.ResponseWriter, r *http.Request) { stat, err := service.GetAggregatedStats(name) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.InternalError(err.Error(), "Failed to get statistics for service "+name+".")) } json.NewEncoder(w).Encode(stat) @@ -39,7 +39,7 @@ func GetAllStat(w http.ResponseWriter, r *http.Request) { all_stat, err := service.GetAllAggregatedStats() if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.InternalError(err.Error(), "Failed to aggregate statistics.")) } json.NewEncoder(w).Encode(all_stat) diff --git a/services/stat/service/stat_service.go b/services/stat/service/stat_service.go index 5cf82516..14dbf157 100644 --- a/services/stat/service/stat_service.go +++ b/services/stat/service/stat_service.go @@ -15,7 +15,7 @@ func GetAggregatedStats(service string) (*models.Stat, error) { endpoint, exists := config.STAT_ENDPOINTS[service] if !exists { - return nil, errors.New("Could not find endpoint for requested stats") + return nil, errors.New("Could not find endpoint for requested stats.") } var stat models.Stat @@ -26,7 +26,7 @@ func GetAggregatedStats(service string) (*models.Stat, error) { } if status != http.StatusOK { - return nil, errors.New("Could not retreive stats from service") + return nil, errors.New("Could not retreive stats from service.") } return &stat, nil diff --git a/services/upload/controller/controller.go b/services/upload/controller/controller.go index a2f8942a..84e0e4d5 100644 --- a/services/upload/controller/controller.go +++ b/services/upload/controller/controller.go @@ -26,7 +26,7 @@ func GetUserResume(w http.ResponseWriter, r *http.Request) { resume, err := service.GetUserResumeLink(id) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.InternalError(err.Error(), "(S3) Cannot fetch user resume link.")) } json.NewEncoder(w).Encode(resume) @@ -41,7 +41,7 @@ func GetCurrentUserResume(w http.ResponseWriter, r *http.Request) { resume, err := service.GetUserResumeLink(id) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.InternalError(err.Error(), "(S3) Cannot fetch user resume link.")) } json.NewEncoder(w).Encode(resume) @@ -56,7 +56,7 @@ func GetUpdateUserResume(w http.ResponseWriter, r *http.Request) { resume, err := service.GetUpdateUserResumeLink(id) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.InternalError(err.Error(), "(S3) Cannot get/update user's resume.")) } json.NewEncoder(w).Encode(resume) diff --git a/services/user/controller/controller.go b/services/user/controller/controller.go index bb854956..6f99a9b9 100644 --- a/services/user/controller/controller.go +++ b/services/user/controller/controller.go @@ -34,7 +34,7 @@ func GetCurrentUserInfo(w http.ResponseWriter, r *http.Request) { user_info, err := service.GetUserInfo(id) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Could not fetch user info by ID.")) } json.NewEncoder(w).Encode(user_info) @@ -48,19 +48,19 @@ func SetUserInfo(w http.ResponseWriter, r *http.Request) { json.NewDecoder(r.Body).Decode(&user_info) if user_info.ID == "" { - panic(errors.UnprocessableError("Must provide id parameter")) + panic(errors.MalformedRequestError("Must provide user id in request.", "Must provide user id in request.")) } err := service.SetUserInfo(user_info.ID, user_info) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Could not upsert user info.")) } updated_info, err := service.GetUserInfo(user_info.ID) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Could not fetch user info by ID.")) } json.NewEncoder(w).Encode(updated_info) @@ -74,7 +74,7 @@ func GetFilteredUserInfo(w http.ResponseWriter, r *http.Request) { user_info, err := service.GetFilteredUserInfo(parameters) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Could not fetch filtered list of users.")) } json.NewEncoder(w).Encode(user_info) @@ -89,7 +89,7 @@ func GetUserInfo(w http.ResponseWriter, r *http.Request) { user_info, err := service.GetUserInfo(id) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Could not fetch user information by user id.")) } json.NewEncoder(w).Encode(user_info) @@ -104,7 +104,7 @@ func GetCurrentQrCodeInfo(w http.ResponseWriter, r *http.Request) { uri, err := service.GetQrInfo(id) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.InternalError(err.Error(), "Could not parse QR code URI.")) } qr_info_container := models.QrInfoContainer{ @@ -124,7 +124,7 @@ func GetQrCodeInfo(w http.ResponseWriter, r *http.Request) { uri, err := service.GetQrInfo(id) if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.DatabaseError(err.Error(), "Could not parse QR code URI.")) } qr_info_container := models.QrInfoContainer{ @@ -142,7 +142,7 @@ func GetStats(w http.ResponseWriter, r *http.Request) { stats, err := service.GetStats() if err != nil { - panic(errors.UnprocessableError(err.Error())) + panic(errors.InternalError(err.Error(), "Could not retrieve user service statistics.")) } json.NewEncoder(w).Encode(stats) diff --git a/services/user/service/user_service.go b/services/user/service/user_service.go index b40afe73..9f7ce06e 100644 --- a/services/user/service/user_service.go +++ b/services/user/service/user_service.go @@ -74,6 +74,7 @@ func GetFilteredUserInfo(parameters map[string][]string) (*models.FilteredUsers, var filtered_users models.FilteredUsers err := db.FindAll("info", query, &filtered_users.Users) + if err != nil { return nil, err } @@ -85,6 +86,11 @@ func GetFilteredUserInfo(parameters map[string][]string) (*models.FilteredUsers, Generates a QR string for a user with the provided ID, as a URI */ func GetQrInfo(id string) (string, error) { + _, err := GetUserInfo(id) + + if err != nil { + return "", errors.New("User does not exist.") + } // Construct the URI diff --git a/utilities/tokengen/tokengen b/utilities/tokengen/tokengen deleted file mode 100644 index 387a4b94..00000000 Binary files a/utilities/tokengen/tokengen and /dev/null differ