diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 index e30e9a9..5fff4c6 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ sftp-config.json *.a *.so gost/gost +debug # Folders _obj diff --git a/LICENSE.md b/LICENSE.md old mode 100644 new mode 100755 diff --git a/README.md b/README.md old mode 100644 new mode 100755 index 93fdf12..c7f1534 --- a/README.md +++ b/README.md @@ -6,11 +6,9 @@ This is the skeleton of a Web API written in [Golang](https://golang.org). In or This template contains basic endpoints for Users (+ login system) and Transactions (payments made between users). Both the endpoints are fully working ones, however the user is free to modify/delete them as they will. -###!NOTE that deleting the Users model completely from the app will make this template to malfunction. - # Configuration steps for the API -1. Install Go and set up your [GOPATH](http://golang.org/doc/code.html#GOPATH) +1. Install Go and set up your [GOPATH](http://golang.org/doc/code.html#GOPATH). Starting with version Go1.4, you also need to set the *GOROOT_BOOTSTRAP* variable, to the same path as your *GOROOT*. 2. Install [MongoDb](https://scotch.io/tutorials/an-introduction-to-mongodb#installation-and-running-mongodb) diff --git a/api/api.go b/api/api.go old mode 100644 new mode 100755 index d792153..d33e19f --- a/api/api.go +++ b/api/api.go @@ -1,4 +1,4 @@ -// Package containing the API functionality helpers +// Package api contains the API functionality helpers // // Each package represents the API functionality of a // certain endpoint which may implement some of the @@ -7,42 +7,55 @@ package api import ( "errors" + "gost/auth/identity" "net/http" "net/url" ) const ( - GET = "GET" - POST = "POST" - PUT = "PUT" + // GET constant represents a GET type http request + GET = "GET" + // POST constant represents a POST type http request + POST = "POST" + // PUT constant represents a PUT type http request + PUT = "PUT" + // DELETE constant represents a DELETE type http request DELETE = "DELETE" ) const ( + // ContentTextPlain represents a HTTP transfer with simple text data ContentTextPlain = "text/plain" - ContentHTML = "text/html" - ContentJSON = "application/json" + // ContentHTML represents a HTTP transfer with html data + ContentHTML = "text/html" + // ContentJSON represents a HTTP transfer with JSON data + ContentJSON = "application/json" ) +// Common errors returned by the API var ( - EntityFormatError = errors.New("The entity was not in the correct format") - EntityIntegrityError = errors.New("The entity doesn't comply to the integrity requirements") - EntityProcessError = errors.New("The entity could not be processed") - EntityNotFoundError = errors.New("No entity with the specified data was found") - - IdParamNotSpecifiedError = errors.New("No id was specified for the entity to be updated") - LimitParamError = errors.New("The limit cannot be 0. Use the value -1 for retrieving all the entities") + ErrEntityFormat = errors.New("The entity was not in the correct format") + ErrEntityIntegrity = errors.New("The entity doesn't comply to the integrity requirements") + ErrEntityProcessing = errors.New("The entity could not be processed") + ErrEntityNotFound = errors.New("No entity with the specified data was found") + ErrIDParamNotSpecified = errors.New("No id was specified for the entity to be updated") + ErrInvalidIDParam = errors.New("The id parameter is not a valid bson.ObjectId") + ErrInvalidInput = errors.New("The needed url paramters were inexistent or invalid") ) -type ApiVar struct { - RequestHeader http.Header - RequestForm url.Values - RequestContentLength int64 - RequestBody []byte +// A Request contains the important and processable data from a HTTP request +type Request struct { + Header http.Header + Form url.Values + ContentLength int64 + Body []byte + Identity *identity.Identity } -type ApiResponse struct { - Message []byte +// A Response contains the information that will be sent back to the user +// through a HTTP response +type Response struct { + Content []byte StatusCode int ErrorMessage string ContentType string diff --git a/api/app/transactionapi/transactionapi.go b/api/app/transactionapi/transactionapi.go new file mode 100755 index 0000000..8607c0c --- /dev/null +++ b/api/app/transactionapi/transactionapi.go @@ -0,0 +1,40 @@ +package transactionapi + +import ( + "gost/api" + "gost/bll" + "gost/filter" + "gost/filter/apifilter" + "gost/orm/models" + "gost/util" +) + +// TransactionsAPI defines the API endpoint for application transactions of any kind +type TransactionsAPI int + +// GetTransaction endpoint retrieves a certain transaction based on its Id +func (t *TransactionsAPI) GetTransaction(params *api.Request) api.Response { + transactionID, found, err := filter.GetIDParameter("transactionId", params.Form) + + if err != nil { + return api.BadRequest(err) + } + + if !found { + return api.NotFound(err) + } + + return bll.GetTransaction(transactionID) +} + +// CreateTransaction endpoint creates a new transaction with the valid transfer tokens and data +func (t *TransactionsAPI) CreateTransaction(params *api.Request) api.Response { + transaction := &models.Transaction{} + + err := util.DeserializeJSON(params.Body, transaction) + if err != nil || !apifilter.CheckTransactionIntegrity(transaction) { + return api.BadRequest(api.ErrEntityFormat) + } + + return bll.CreateTransaction(transaction) +} diff --git a/api/app/transactionapi/transactionapi_test.go b/api/app/transactionapi/transactionapi_test.go new file mode 100755 index 0000000..f8a25b3 --- /dev/null +++ b/api/app/transactionapi/transactionapi_test.go @@ -0,0 +1,108 @@ +package transactionapi + +import ( + "gost/api" + "gost/auth/identity" + "gost/orm/models" + "gost/service/transactionservice" + "gost/tests" + "net/http" + "net/url" + "testing" + + "gopkg.in/mgo.v2/bson" +) + +const ( + ActionGet = "GetTransaction" + ActionCreate = "CreateTransaction" +) + +const endpointPath = "/transactions" + +type dummyTransaction struct { + BadField string +} + +func TestTransactionsApi(t *testing.T) { + var id bson.ObjectId + + tests.InitializeServerConfigurations(new(TransactionsAPI)) + + // Cleanup function + defer func() { + recover() + transactionservice.DeleteTransaction(id) + }() + + testPostTransactionInBadFormat(t) + testPostTransactionNotIntegral(t) + id = testPostTransactionInGoodFormat(t) + testGetTransactionWithInexistentIDInDB(t) + testGetTransactionWithBadIDParam(t) + testGetTransactionWithGoodIDParam(t, id) +} + +func testGetTransactionWithInexistentIDInDB(t *testing.T) { + params := url.Values{} + params.Add("transactionId", bson.NewObjectId().Hex()) + + tests.PerformTestRequest(endpointPath, ActionGet, api.GET, http.StatusNotFound, params, nil, t) +} + +func testGetTransactionWithBadIDParam(t *testing.T) { + params := url.Values{} + params.Add("transactionId", "2as456fas4") + + tests.PerformTestRequest(endpointPath, ActionGet, api.GET, http.StatusBadRequest, params, nil, t) +} + +func testGetTransactionWithGoodIDParam(t *testing.T, id bson.ObjectId) { + params := url.Values{} + params.Add("transactionId", id.Hex()) + + rw := tests.PerformTestRequest(endpointPath, ActionGet, api.GET, http.StatusOK, params, nil, t) + + body := rw.Body.String() + if len(body) == 0 { + t.Error("Response body is empty or in a corrupt format:", body) + } +} + +func testPostTransactionInBadFormat(t *testing.T) { + dTransaction := &dummyTransaction{ + BadField: "bad value", + } + + tests.PerformTestRequest(endpointPath, ActionCreate, api.POST, http.StatusBadRequest, nil, dTransaction, t) +} + +func testPostTransactionNotIntegral(t *testing.T) { + transaction := &models.Transaction{ + ID: bson.NewObjectId(), + Payer: identity.ApplicationUser{ID: bson.NewObjectId()}, + Currency: "USD", + } + + tests.PerformTestRequest(endpointPath, ActionCreate, api.POST, http.StatusBadRequest, nil, transaction, t) +} + +func testPostTransactionInGoodFormat(t *testing.T) bson.ObjectId { + transaction := &models.Transaction{ + ID: bson.NewObjectId(), + Payer: identity.ApplicationUser{ID: bson.NewObjectId()}, + Receiver: identity.ApplicationUser{ID: bson.NewObjectId()}, + Type: models.TransactionTypeCash, + Ammount: 216.365, + Currency: "USD", + } + + rw := tests.PerformTestRequest(endpointPath, ActionCreate, api.POST, http.StatusCreated, nil, transaction, t) + + body := rw.Body.String() + if len(body) == 0 { + t.Error("Response body is empty or in deteriorated format:", body) + } + + return transaction.ID +} diff --git a/api/basic_responses.go b/api/basic_responses.go old mode 100644 new mode 100755 index 8bf6de1..a485eb3 --- a/api/basic_responses.go +++ b/api/basic_responses.go @@ -1,51 +1,56 @@ package api import ( - "gost/models" + "gost/util" "io/ioutil" ) -func SingleDataResponse(statusCode int, data models.Modeler) ApiResponse { - jsonData, err := models.SerializeJson(data) +// JSONResponse creates a Response from the api, containing a single entity encoded as JSON +func JSONResponse(statusCode int, data interface{}) Response { + jsonData, err := util.SerializeJSON(data) if err != nil { return InternalServerError(err) } - return ApiResponse{ + return Response{ StatusCode: statusCode, - Message: jsonData, + Content: jsonData, } } -func MultipleDataResponse(statusCode int, data []models.Modeler) ApiResponse { - jsonData, err := models.SerializeJson(data) - if err != nil { - return InternalServerError(err) - } +// StatusResponse creates a Response from the api, containing just a status code +func StatusResponse(statusCode int) Response { + return Response{StatusCode: statusCode} +} - return ApiResponse{ - StatusCode: statusCode, - Message: jsonData, - } +// PlainTextResponse creates a Response from the api, containing a status code and a text message +func PlainTextResponse(statusCode int, text string) Response { + return DataResponse(statusCode, []byte(text), ContentTextPlain) } -func StatusResponse(statusCode int) ApiResponse { - return ApiResponse{StatusCode: statusCode} +// TextResponse creates a Response from the api, containing a status code, a text message and a custom content-type +func TextResponse(statusCode int, text, contentType string) Response { + return DataResponse(statusCode, []byte(text), contentType) } -func ByteResponse(statusCode int, data []byte) ApiResponse { - return ApiResponse{ - StatusCode: statusCode, - Message: data, +// DataResponse creates a Response from the api, containing a status code, +// a custom content-type and a message in the form of a byte array +func DataResponse(statusCode int, data []byte, contetType string) Response { + return Response{ + StatusCode: statusCode, + Content: data, + ContentType: contetType, } } -func FileResponse(statusCode int, contentType, fullFilePath string) ApiResponse { +// FileResponse creates a Response from the api, containing a file path (download, load or stream) +// and the content type of the file that is returned +func FileResponse(statusCode int, contentType, fullFilePath string) Response { if _, err := ioutil.ReadFile(fullFilePath); err != nil { return InternalServerError(err) } - return ApiResponse{ + return Response{ StatusCode: statusCode, File: fullFilePath, ContentType: contentType, diff --git a/api/custom_statuses.go b/api/custom_statuses.go new file mode 100755 index 0000000..4bc7aab --- /dev/null +++ b/api/custom_statuses.go @@ -0,0 +1,22 @@ +package api + +import "net/http" + +const ( + // StatusTooManyRequests is used for issuing errors regarding the number of request made by a client + StatusTooManyRequests = 429 +) + +var statusText = map[int]string{ + StatusTooManyRequests: "Too many requests", +} + +// StatusText returns the message associated to a http status code +func StatusText(statusCode int) string { + msg := http.StatusText(statusCode) + if len(msg) > 0 { + return msg + } + + return statusText[statusCode] +} diff --git a/api/error_responses.go b/api/error_responses.go old mode 100644 new mode 100755 index 1e0e318..0c738f4 --- a/api/error_responses.go +++ b/api/error_responses.go @@ -1,61 +1,61 @@ package api import ( - "net/http" + "net/http" ) -// Return a status and message that signals the API client +// InternalServerError returns a status and message that signals the API client // about an 'internal server error' that has occured -func InternalServerError(err error) ApiResponse { - return ApiResponse{ - StatusCode: http.StatusInternalServerError, - ErrorMessage: err.Error(), - } +func InternalServerError(err error) Response { + return Response{ + StatusCode: http.StatusInternalServerError, + ErrorMessage: err.Error(), + } } -// Return a status and message that signals the API client +// BadRequest returns a status and message that signals the API client // about a 'bad request' that the client has made to the server -func BadRequest(err error) ApiResponse { - return ApiResponse{ - StatusCode: http.StatusBadRequest, - ErrorMessage: err.Error(), - } +func BadRequest(err error) Response { + return Response{ + StatusCode: http.StatusBadRequest, + ErrorMessage: err.Error(), + } } -// Return a status and message that signals the API client +// NotFound returns a status and message that signals the API client // that the searched resource was not found on the server -func NotFound(err error) ApiResponse { - return ApiResponse{ - StatusCode: http.StatusNotFound, - ErrorMessage: http.StatusText(http.StatusNotFound), - } +func NotFound(err error) Response { + return Response{ + StatusCode: http.StatusNotFound, + ErrorMessage: StatusText(http.StatusNotFound), + } } -// Return a status and message that signals the API client +// ServiceUnavailable returns a status and message that signals the API client // that the accessed endpoint is either disabled or currently // unavailable -func ServiceUnavailable(err error) ApiResponse { - return ApiResponse{ - StatusCode: http.StatusServiceUnavailable, - ErrorMessage: err.Error(), - } +func ServiceUnavailable(err error) Response { + return Response{ + StatusCode: http.StatusServiceUnavailable, + ErrorMessage: err.Error(), + } } -// Return a status and message that signals the API client +// MethodNotAllowed returns a status and message that signals the API client // that the used HTTP Method is not allowed on this endpoint -func MethodNotAllowed(err error) ApiResponse { - return ApiResponse{ - StatusCode: http.StatusMethodNotAllowed, - ErrorMessage: err.Error(), - } +func MethodNotAllowed() Response { + return Response{ + StatusCode: http.StatusMethodNotAllowed, + ErrorMessage: StatusText(http.StatusMethodNotAllowed), + } } -// Return a status and message that signals the API client +// Unauthorized returns a status and message that signals the API client // that the login failed or that the client isn't // logged in and therefore not authorized to use the endpoint -func Unauthorized(err error) ApiResponse { - return ApiResponse{ - StatusCode: http.StatusUnauthorized, - ErrorMessage: err.Error(), - } +func Unauthorized() Response { + return Response{ + StatusCode: http.StatusUnauthorized, + ErrorMessage: StatusText(http.StatusUnauthorized), + } } diff --git a/api/framework/authapi/authapi.go b/api/framework/authapi/authapi.go new file mode 100755 index 0000000..ab24de7 --- /dev/null +++ b/api/framework/authapi/authapi.go @@ -0,0 +1,90 @@ +package authapi + +import ( + "errors" + "gost/api" + "gost/auth" + "gost/util" + "net/http" +) + +// AuthAPI defines the API endpoint for user authorization +type AuthAPI int + +var errPasswordsDoNotMatch = errors.New("The password and its confirmation do not match") + +// ActivateAccount activates an account using the activation token sent through email +func (a *AuthAPI) ActivateAccount(params *api.Request) api.Response { + var model = ActivateAccountModel{} + + var err = util.DeserializeJSON(params.Body, &model) + if err != nil { + return api.BadRequest(api.ErrEntityFormat) + } + + err = auth.ActivateAppUser(model.Token) + if err != nil { + return api.BadRequest(err) + } + + return api.StatusResponse(http.StatusOK) +} + +// ResendAccountActivationEmail resends the email with the details for activating their user account +func (a *AuthAPI) ResendAccountActivationEmail(params *api.Request) api.Response { + var model = ResendActivationEmailModel{} + + var err = util.DeserializeJSON(params.Body, &model) + if err != nil { + return api.BadRequest(api.ErrEntityFormat) + } + + err = auth.ResendAccountActivationEmail(model.Email, model.ActivateAccountServiceLink) + if err != nil { + return api.InternalServerError(err) + } + + return api.StatusResponse(http.StatusOK) +} + +// RequestResetPassword sends an email with a special token that will be used for resetting the password +func (a *AuthAPI) RequestResetPassword(params *api.Request) api.Response { + var model = RequestResetPasswordModel{} + + var err = util.DeserializeJSON(params.Body, &model) + if err != nil { + return api.BadRequest(api.ErrEntityFormat) + } + + err = auth.RequestResetPassword(model.Email, model.PasswordResetServiceLink) + if err != nil { + return api.InternalServerError(err) + } + + return api.StatusResponse(http.StatusOK) +} + +// ResetPassword resets an user account's password +func (a *AuthAPI) ResetPassword(params *api.Request) api.Response { + var model = ResetPasswordModel{} + + var err = util.DeserializeJSON(params.Body, &model) + if err != nil { + return api.BadRequest(api.ErrEntityFormat) + } + + if model.Password != model.PasswordConfirmation { + return api.BadRequest(errPasswordsDoNotMatch) + } + + err = auth.ResetPassword(model.Token, model.Password) + if err != nil { + if err == auth.ErrResetPasswordTokenExpired { + return api.BadRequest(err) + } + + return api.InternalServerError(err) + } + + return api.StatusResponse(http.StatusOK) +} diff --git a/api/framework/authapi/models.go b/api/framework/authapi/models.go new file mode 100755 index 0000000..b5046a9 --- /dev/null +++ b/api/framework/authapi/models.go @@ -0,0 +1,25 @@ +package authapi + +// ActivateAccountModel is used for activating accounts +type ActivateAccountModel struct { + Token string `json:"token"` +} + +// RequestResetPasswordModel is used for requesting password resets over email +type RequestResetPasswordModel struct { + Email string `json:"email"` + PasswordResetServiceLink string `json:"passwordResetServiceLink"` +} + +// ResetPasswordModel is used for resetting the account password +type ResetPasswordModel struct { + Token string `json:"token"` + Password string `json:"password"` + PasswordConfirmation string `json:"passwordConfirmation"` +} + +// ResendActivationEmailModel is used for resending the account activation email +type ResendActivationEmailModel struct { + Email string `json:"email"` + ActivateAccountServiceLink string `json:"activateAccountServiceLink"` +} diff --git a/api/framework/authapi/sessionapi.go b/api/framework/authapi/sessionapi.go new file mode 100755 index 0000000..cc53de5 --- /dev/null +++ b/api/framework/authapi/sessionapi.go @@ -0,0 +1,91 @@ +package authapi + +import ( + "errors" + "gost/api" + "gost/auth" + "gost/auth/cookies" + "gost/filter" + "gost/util" + "net/http" + + "gopkg.in/mgo.v2/bson" +) + +// Errors generated by then Auth endpoint +var ( + ErrPasswordMatch = errors.New("The passwords do not match") + ErrTokenNotSpecified = errors.New("The session token was not specified") +) + +// AuthModel is a binding model used for receiving the authentication data +type AuthModel struct { + AppUserID string `json:"appUserID"` + Password string `json:"password"` + PasswordConfirmation string `json:"passwordConfirmation"` + ClientDetails *cookies.Client `json:"clientDetails"` +} + +// GetAllSessions retrieves all the sessions for a certain user account +func (a *AuthAPI) GetAllSessions(params *api.Request) api.Response { + userID, found, err := filter.GetIDParameter("token", params.Form) + if !found { + return api.BadRequest(api.ErrIDParamNotSpecified) + } + if err != nil { + return api.InternalServerError(err) + } + + userSessions, err := cookies.GetUserSessions(userID) + if err != nil { + return api.InternalServerError(err) + } + + return api.JSONResponse(http.StatusOK, userSessions) +} + +// CreateSession creates a new session for an existing user account +func (a *AuthAPI) CreateSession(params *api.Request) api.Response { + model := &AuthModel{} + + err := util.DeserializeJSON(params.Body, model) + if err != nil { + return api.BadRequest(err) + } + + if model.Password != model.PasswordConfirmation { + return api.BadRequest(ErrPasswordMatch) + } + + if !bson.IsObjectIdHex(model.AppUserID) { + return api.BadRequest(api.ErrInvalidIDParam) + } + + token, err := auth.GenerateUserAuth(bson.ObjectIdHex(model.AppUserID), model.ClientDetails) + if err != nil { + return api.BadRequest(err) + } + + return api.PlainTextResponse(http.StatusOK, token) +} + +// KillSession deletes a session for an existing user account based on +// the session token +func (a *AuthAPI) KillSession(params *api.Request) api.Response { + sessionToken, found := filter.GetStringParameter("token", params.Form) + if !found || len(sessionToken) == 0 { + return api.BadRequest(ErrTokenNotSpecified) + } + + session, err := cookies.GetSession(sessionToken) + if err != nil { + return api.InternalServerError(err) + } + + err = session.Delete() + if err != nil { + return api.InternalServerError(err) + } + + return api.StatusResponse(http.StatusOK) +} diff --git a/api/framework/devapi/devapi.go b/api/framework/devapi/devapi.go new file mode 100755 index 0000000..f4659b0 --- /dev/null +++ b/api/framework/devapi/devapi.go @@ -0,0 +1,54 @@ +package devapi + +import ( + "gost/api" + "gost/auth" + "gost/config" + "gost/filter" + "gost/util" + "net/http" +) + +// DevAPI defines the API endpoint for development actions and custom testing +type DevAPI int + +// AppUserModel is the model used for creating ApplicationUsers +type AppUserModel struct { + Email string `json:"email"` + Password string `json:"password"` + AccountType int `json:"accountType"` // 0 - NormalUser | 1 - Admin +} + +// CreateAppUser is an endpoint used for creating application users +func (v *DevAPI) CreateAppUser(params *api.Request) api.Response { + model := &AppUserModel{} + + err := util.DeserializeJSON(params.Body, model) + if err != nil { + return api.BadRequest(api.ErrEntityFormat) + } + + var activationServiceLink = config.HTTPServerAddress + config.APIInstance + "dev/ActivateAppUser?token=%s" + + user, err := auth.CreateAppUser(model.Email, model.Password, model.AccountType, activationServiceLink) + if err != nil { + return api.InternalServerError(err) + } + + return api.JSONResponse(http.StatusOK, user) +} + +// ActivateAppUser is an endpoint for activating an app user +func (v *DevAPI) ActivateAppUser(params *api.Request) api.Response { + var token, found = filter.GetStringParameter("token", params.Form) + if !found { + return api.BadRequest(api.ErrInvalidInput) + } + + var err = auth.ActivateAppUser(token) + if err != nil { + return api.BadRequest(err) + } + + return api.PlainTextResponse(http.StatusOK, "Account is now active") +} diff --git a/api/framework/devapi/init.go b/api/framework/devapi/init.go new file mode 100755 index 0000000..2423b7f --- /dev/null +++ b/api/framework/devapi/init.go @@ -0,0 +1,28 @@ +package devapi + +import ( + "encoding/json" + "gost/config" + "io/ioutil" + "log" +) + +// Routes configuration file path +var routesConfigFile = "config/devroutes.json" + +// InitDevRoutes initializes the routes used for development purposes only +func InitDevRoutes() { + routesString, err := ioutil.ReadFile(routesConfigFile) + if err != nil { + log.Fatal(err) + } + + var route = config.Route{} + + err = json.Unmarshal(routesString, &route) + if err != nil { + log.Fatal(err) + } + + config.Routes = append(config.Routes, route) +} diff --git a/api/framework/valuesapi/valuesapi.go b/api/framework/valuesapi/valuesapi.go new file mode 100755 index 0000000..5cfaffd --- /dev/null +++ b/api/framework/valuesapi/valuesapi.go @@ -0,0 +1,43 @@ +package valuesapi + +import ( + "bytes" + "gost/api" + "net/http" +) + +// ValuesAPI defines the API endpoint for verifying the API status of the application +type ValuesAPI int + +// Get performs a HTTP GET as an authorized user +func (v *ValuesAPI) Get(params *api.Request) api.Response { + var message bytes.Buffer + + message.WriteString("You are currently authorized.\nYour role is: ") + if params.Identity.IsAdmin() { + message.WriteString("ADMIN") + } else { + message.WriteString("NORMAL USER") + } + + return api.PlainTextResponse(http.StatusOK, message.String()) +} + +// GetAnonymous performs a HTTP GET as an anonymous user +func (v *ValuesAPI) GetAnonymous(params *api.Request) api.Response { + var message bytes.Buffer + status := http.StatusOK + + message.WriteString("You have accessed an endpoint action available for anonymous users.\n") + + if params.Identity.IsAuthorized() { + message.WriteString("BTW, You are an authorized user") + } else if !params.Identity.IsAnonymous() { + message.WriteString("Cannot verify your authorization status, something is wrong") + status = http.StatusForbidden + } else { + message.WriteString("BTW, You are an anonymous user") + } + + return api.PlainTextResponse(status, message.String()) +} diff --git a/api/transactionapi/transactions_api.go b/api/transactionapi/transactions_api.go deleted file mode 100644 index 52f1006..0000000 --- a/api/transactionapi/transactions_api.go +++ /dev/null @@ -1,60 +0,0 @@ -package transactionapi - -import ( - "gost/api" - "gost/filter/apifilter" - "gost/models" - "gost/service/transactionservice" - "net/http" -) - -type TransactionsApi int - -const ApiName = "transactions" - -func (t *TransactionsApi) GetTransaction(vars *api.ApiVar) api.ApiResponse { - transactionId, err, found := apifilter.GetIdFromParams(vars.RequestForm) - if found { - if err != nil { - return api.BadRequest(err) - } - - dbTransaction, err := transactionservice.GetTransaction(transactionId) - if err != nil || dbTransaction == nil { - return api.NotFound(api.EntityNotFoundError) - } - - transaction := &models.Transaction{} - transaction.Expand(dbTransaction) - - return api.SingleDataResponse(http.StatusOK, transaction) - } - - return api.BadRequest(api.IdParamNotSpecifiedError) -} - -func (t *TransactionsApi) PostTransaction(vars *api.ApiVar) api.ApiResponse { - transaction := &models.Transaction{} - - err := models.DeserializeJson(vars.RequestBody, transaction) - if err != nil { - return api.BadRequest(api.EntityFormatError) - } - - if !apifilter.CheckTransactionIntegrity(transaction) { - return api.BadRequest(api.EntityIntegrityError) - } - - dbTransaction := transaction.Collapse() - if dbTransaction == nil { - return api.InternalServerError(api.EntityProcessError) - } - - err = transactionservice.CreateTransaction(dbTransaction) - if err != nil { - return api.InternalServerError(api.EntityProcessError) - } - transaction.Id = dbTransaction.Id - - return api.SingleDataResponse(http.StatusCreated, transaction) -} diff --git a/api/transactionapi/transactions_api_test.go b/api/transactionapi/transactions_api_test.go deleted file mode 100644 index fa3b67b..0000000 --- a/api/transactionapi/transactions_api_test.go +++ /dev/null @@ -1,100 +0,0 @@ -package transactionapi - -import ( - "gopkg.in/mgo.v2/bson" - "gost/api" - "gost/dbmodels" - "gost/models" - "gost/service/transactionservice" - "gost/tests" - "net/http" - "net/url" - "testing" -) - -const transactionsRoute = "[{\"id\": \"TransactionsRoute\", \"pattern\": \"/transactions\", \"handlers\": {\"DELETE\": \"DeleteTransaction\", \"GET\": \"GetTransaction\", \"POST\": \"PostTransaction\"}}]" -const apiPath = "/transactions" - -type dummyTransaction struct { - BadField string -} - -func (transaction *dummyTransaction) PopConstrains() {} - -func TestTransactionsApi(t *testing.T) { - tests.InitializeServerConfigurations(transactionsRoute, new(TransactionsApi)) - - testPostTransactionInBadFormat(t) - testPostTransactionNotIntegral(t) - id := testPostTransactionInGoodFormat(t) - testGetTransactionWithInexistentIdInDB(t) - testGetTransactionWithBadIdParam(t) - testGetTransactionWithGoodIdParam(t, id) - - // Delete the created transaction - transactionservice.DeleteTransaction(id) -} - -func testGetTransactionWithInexistentIdInDB(t *testing.T) { - params := url.Values{} - params.Add("id", bson.NewObjectId().Hex()) - - tests.PerformApiTestCall(apiPath, api.GET, http.StatusNotFound, params, nil, t) -} - -func testGetTransactionWithBadIdParam(t *testing.T) { - params := url.Values{} - params.Add("id", "2as456fas4") - - tests.PerformApiTestCall(apiPath, api.GET, http.StatusBadRequest, params, nil, t) -} - -func testGetTransactionWithGoodIdParam(t *testing.T, id bson.ObjectId) { - params := url.Values{} - params.Add("id", id.Hex()) - - rw := tests.PerformApiTestCall(apiPath, api.GET, http.StatusOK, params, nil, t) - - body := rw.Body.String() - if len(body) == 0 { - t.Error("Response body is empty or in deteriorated format:", body) - } -} - -func testPostTransactionInBadFormat(t *testing.T) { - dTransaction := &dummyTransaction{ - BadField: "bad value", - } - - tests.PerformApiTestCall(apiPath, api.POST, http.StatusBadRequest, nil, dTransaction, t) -} - -func testPostTransactionNotIntegral(t *testing.T) { - transaction := &models.Transaction{ - Id: bson.NewObjectId(), - Payer: models.User{Id: bson.NewObjectId()}, - Currency: "USD", - } - - tests.PerformApiTestCall(apiPath, api.POST, http.StatusBadRequest, nil, transaction, t) -} - -func testPostTransactionInGoodFormat(t *testing.T) bson.ObjectId { - transaction := &models.Transaction{ - Id: bson.NewObjectId(), - Payer: models.User{Id: bson.NewObjectId()}, - Receiver: models.User{Id: bson.NewObjectId()}, - Type: dbmodels.CASH_TRANSACTION_TYPE, - Ammount: 216.365, - Currency: "USD", - } - - rw := tests.PerformApiTestCall(apiPath, api.POST, http.StatusCreated, nil, transaction, t) - - body := rw.Body.String() - if len(body) == 0 { - t.Error("Response body is empty or in deteriorated format:", body) - } - - return transaction.Id -} diff --git a/api/userapi/users_api.go b/api/userapi/users_api.go deleted file mode 100644 index 50f60f4..0000000 --- a/api/userapi/users_api.go +++ /dev/null @@ -1,163 +0,0 @@ -package userapi - -import ( - "gopkg.in/mgo.v2/bson" - "gost/api" - "gost/dbmodels" - "gost/filter/apifilter" - "gost/models" - "gost/service/userservice" - "net/http" - "strings" -) - -type UsersApi int - -const ApiName = "users" - -func (usersApi *UsersApi) GetUser(vars *api.ApiVar) api.ApiResponse { - userId, err, found := apifilter.GetIdFromParams(vars.RequestForm) - if found { - if err != nil { - return api.BadRequest(err) - } - - return getUser(vars, userId) - } - - limit, err, found := apifilter.GetIntValueFromParams("limit", vars.RequestForm) - if found { - if err != nil { - return api.BadRequest(err) - } - - return getAllUsers(vars, limit) - } - - return getAllUsers(vars, -1) - -} - -func (usersApi *UsersApi) PostUser(vars *api.ApiVar) api.ApiResponse { - user := &models.User{} - - err := models.DeserializeJson(vars.RequestBody, user) - if err != nil { - return api.BadRequest(api.EntityFormatError) - } - - if !apifilter.CheckUserIntegrity(user) { - return api.BadRequest(api.EntityIntegrityError) - } - - dbUser := user.Collapse() - if dbUser == nil { - return api.InternalServerError(api.EntityProcessError) - } - - err = userservice.CreateUser(dbUser) - if err != nil { - return api.InternalServerError(api.EntityProcessError) - } - user.Id = dbUser.Id - - return api.SingleDataResponse(http.StatusCreated, user) -} - -func (usersApi *UsersApi) PutUser(vars *api.ApiVar) api.ApiResponse { - user := &models.User{} - err := models.DeserializeJson(vars.RequestBody, user) - - if err != nil { - return api.BadRequest(api.EntityFormatError) - } - - if user.Id == "" { - return api.BadRequest(api.IdParamNotSpecifiedError) - } - - if !apifilter.CheckUserIntegrity(user) { - return api.BadRequest(api.EntityIntegrityError) - } - - dbUser := user.Collapse() - if dbUser == nil { - return api.InternalServerError(api.EntityProcessError) - } - - err = userservice.UpdateUser(dbUser) - if err != nil { - return api.NotFound(api.EntityNotFoundError) - } - - return api.SingleDataResponse(http.StatusOK, user) -} - -func (usersApi *UsersApi) DeleteUser(vars *api.ApiVar) api.ApiResponse { - userId, err, found := apifilter.GetIdFromParams(vars.RequestForm) - - if found { - if err != nil { - return api.BadRequest(err) - } - - err = userservice.DeleteUser(userId) - if err != nil { - return api.NotFound(err) - } - - return api.StatusResponse(http.StatusOK) - } - - return api.BadRequest(err) -} - -func getAllUsers(vars *api.ApiVar, limit int) api.ApiResponse { - var dbUsers []dbmodels.User - var err error - - if limit == 0 { - return api.BadRequest(api.LimitParamError) - } - - if limit != -1 { - dbUsers, err = userservice.GetAllUsersLimited(limit) - } else { - dbUsers, err = userservice.GetAllUsers() - } - - if err != nil { - return api.InternalServerError(err) - } - - users := make([]models.Modeler, len(dbUsers)) - for i := 0; i < len(dbUsers); i++ { - user := &models.User{} - user.Expand(&dbUsers[i]) - - users[i] = user - } - - return api.MultipleDataResponse(http.StatusOK, users) -} - -func getUser(vars *api.ApiVar, userId bson.ObjectId) api.ApiResponse { - dbUser, err := userservice.GetUser(userId) - - if err != nil { - if strings.Contains(strings.ToLower(err.Error()), "found") { - return api.NotFound(err) - } else { - return api.InternalServerError(err) - } - } - - if dbUser == nil { - return api.NotFound(api.EntityNotFoundError) - } - - user := &models.User{} - user.Expand(dbUser) - - return api.SingleDataResponse(http.StatusOK, user) -} diff --git a/api/userapi/users_api_test.go b/api/userapi/users_api_test.go deleted file mode 100644 index c5993c3..0000000 --- a/api/userapi/users_api_test.go +++ /dev/null @@ -1,214 +0,0 @@ -package userapi - -import ( - "gopkg.in/mgo.v2/bson" - "gost/api" - "gost/dbmodels" - "gost/models" - "gost/tests" - "net/http" - "net/url" - "testing" -) - -const usersRoute = "[{\"id\": \"UsersRoute\", \"pattern\": \"/users\", \"handlers\": {\"DELETE\": \"DeleteUser\", \"GET\": \"GetUser\", \"POST\": \"PostUser\", \"PUT\": \"PutUser\"}}]" -const apiPath = "/users" - -type dummyUser struct { - BadField string -} - -func (user *dummyUser) PopConstrains() {} - -func TestUsersApi(t *testing.T) { - tests.InitializeServerConfigurations(usersRoute, new(UsersApi)) - - testPostUserInBadFormat(t) - id := testPostUserInGoodFormat(t) - testPutUserInBadFormat(t) - testPutUserWithoutId(t) - testPutUserWithNoExistentIdInDb(t) - testPutUserWithGoodRequestDetails(t, id) - testGetUserWithInexistentIdInDB(t) - testGetUserWithBadIdParam(t) - testGetUserWithGoodIdParam(t, id) - testGetAllUsersWithoutLimit(t) - testGetAllUsersWithBadLimitParam(t) - testGetAllUsersWithGoodLimitParam(t) - testDeleteUserWithNoIdParam(t) - testDeleteUserWithIdParamInWrongFormat(t) - testDeteleUserWithInexistentIdInDB(t) - testDeteleUserWithGoodRequestParams(t, id) -} - -func testGetUserWithInexistentIdInDB(t *testing.T) { - params := url.Values{} - params.Add("id", bson.NewObjectId().Hex()) - - tests.PerformApiTestCall(apiPath, api.GET, http.StatusNotFound, params, nil, t) -} - -func testGetUserWithBadIdParam(t *testing.T) { - params := url.Values{} - params.Add("id", "2as456fas4") - - tests.PerformApiTestCall(apiPath, api.GET, http.StatusBadRequest, params, nil, t) -} - -func testGetUserWithGoodIdParam(t *testing.T, id bson.ObjectId) { - params := url.Values{} - params.Add("id", id.Hex()) - - rw := tests.PerformApiTestCall(apiPath, api.GET, http.StatusOK, params, nil, t) - - body := rw.Body.String() - if len(body) == 0 { - t.Error("Response body is empty or in deteriorated format:", body) - } - -} - -func testGetAllUsersWithoutLimit(t *testing.T) { - rw := tests.PerformApiTestCall(apiPath, api.GET, http.StatusOK, nil, nil, t) - - body := rw.Body.String() - if len(body) == 0 { - t.Error("Response body is empty or in deteriorated format:", body) - } -} - -func testGetAllUsersWithBadLimitParam(t *testing.T) { - params := url.Values{} - params.Add("limit", "asfsa") - - tests.PerformApiTestCall(apiPath, api.GET, http.StatusBadRequest, params, nil, t) -} - -func testGetAllUsersWithGoodLimitParam(t *testing.T) { - params := url.Values{} - params.Add("limit", "20") - - rw := tests.PerformApiTestCall(apiPath, api.GET, http.StatusOK, params, nil, t) - - body := rw.Body.String() - if len(body) == 0 { - t.Error("Response body is empty or in deteriorated format:", body) - } -} - -func testPostUserInBadFormat(t *testing.T) { - dUser := &dummyUser{ - BadField: "bad value", - } - - tests.PerformApiTestCall(apiPath, api.POST, http.StatusBadRequest, nil, dUser, t) -} - -func testPostUserInGoodFormat(t *testing.T) bson.ObjectId { - user := &models.User{ - Id: bson.NewObjectId(), - Password: "CoddoPass", - AccountType: dbmodels.ADMINISTRATOR_ACCOUNT_TYPE, - FirstName: "Claudiu", - LastName: "Codoban", - Email: "test@tests.com", - Sex: 'M', - Country: "Romania", - State: "Hunedoara", - City: "Deva", - Address: "AddrTest", - PostalCode: 330099, - Picture: "ftp://pictLink", - Token: "as7f6as8faf5aasf6721rqf", - } - - rw := tests.PerformApiTestCall(apiPath, api.POST, http.StatusCreated, nil, user, t) - - body := rw.Body.String() - if len(body) == 0 { - t.Error("Response body is empty or in deteriorated format:", body) - } - - return user.Id -} - -func testPutUserInBadFormat(t *testing.T) { - user := &models.User{ - Id: "507f191e810c19729de860ea", - Sex: 'M', - FirstName: "gigel", - Country: "Romania", - } - - tests.PerformApiTestCall(apiPath, api.PUT, http.StatusBadRequest, nil, user, t) -} - -func testPutUserWithoutId(t *testing.T) { - user := &models.User{ - Email: "ceva@ceva.com", - Sex: 'M', - FirstName: "gigel", - Token: "fsa4fas564g6g4s6ag", - Country: "Romania", - } - - tests.PerformApiTestCall(apiPath, api.PUT, http.StatusBadRequest, nil, user, t) -} - -func testPutUserWithNoExistentIdInDb(t *testing.T) { - user := &models.User{ - Id: bson.NewObjectId(), - Email: "ceva@ceva.com", - Sex: 'M', - FirstName: "gigel", - Token: "fsa4fas564g6g4s6ag", - Country: "Romania", - Address: "addr", - } - - tests.PerformApiTestCall(apiPath, api.PUT, http.StatusNotFound, nil, user, t) -} - -func testPutUserWithGoodRequestDetails(t *testing.T, id bson.ObjectId) { - user := &models.User{ - Id: id, - Email: "ceva@ceva.com", - Sex: 'M', - FirstName: "gigel", - Token: "fsa4fas564g6g4s6ag", - Country: "Romania", - Address: "addr", - } - - rw := tests.PerformApiTestCall(apiPath, api.PUT, http.StatusOK, nil, user, t) - body := rw.Body.String() - - if len(body) == 0 { - t.Fatal("The response body was wither empty or deteriorated", body) - } -} - -func testDeleteUserWithNoIdParam(t *testing.T) { - tests.PerformApiTestCall(apiPath, api.DELETE, http.StatusBadRequest, nil, nil, t) -} - -func testDeleteUserWithIdParamInWrongFormat(t *testing.T) { - params := url.Values{} - params.Add("id", "a46fsa65gas") - - tests.PerformApiTestCall(apiPath, api.DELETE, http.StatusBadRequest, params, nil, t) -} - -func testDeteleUserWithInexistentIdInDB(t *testing.T) { - params := url.Values{} - params.Add("id", bson.NewObjectId().Hex()) - - tests.PerformApiTestCall(apiPath, api.DELETE, http.StatusNotFound, params, nil, t) -} - -func testDeteleUserWithGoodRequestParams(t *testing.T, id bson.ObjectId) { - params := url.Values{} - params.Add("id", id.Hex()) - - tests.PerformApiTestCall(apiPath, api.DELETE, http.StatusNoContent, params, nil, t) -} diff --git a/api/userloginapi/user_login_api.go b/api/userloginapi/user_login_api.go deleted file mode 100644 index 99e8829..0000000 --- a/api/userloginapi/user_login_api.go +++ /dev/null @@ -1,83 +0,0 @@ -package userloginapi - -import ( - "errors" - "gost/api" - "gost/filter/apifilter" - "gost/models" - "gost/service/userloginservice" - "net/http" - "time" -) - -type UserSessionsApi int - -const ApiName = "userSessions" - -var ( - TokenNotSpecifiedError = errors.New("The session token hasn't been specified") - TokenExpiredError = errors.New("The session with the specified token has expired") -) - -const ( - daysUntilExpire = 7 -) - -func (userSessionsApi *UserSessionsApi) GetUserSession(vars *api.ApiVar) api.ApiResponse { - token, found := apifilter.GetStringValueFromParams("token", vars.RequestForm) - today := time.Now().Local() - - if !found { - return api.BadRequest(TokenNotSpecifiedError) - } - - dbUserSession, err := userloginservice.GetUserSession(token) - if err != nil { - return api.NotFound(err) - } else if dbUserSession.ExpireDate.Local().Before(today) { - userloginservice.DeleteUserSession(dbUserSession.Id) - return api.Unauthorized(TokenExpiredError) - } - dbUserSession.ExpireDate = today.Add(time.Hour * 24 * daysUntilExpire) - - err = userloginservice.UpdateUserSession(dbUserSession) - if err != nil { - return api.InternalServerError(err) - } - - userSession := new(models.UserSession) - userSession.Expand(dbUserSession) - - userloginservice.DeleteExpiredSessionsForUser(dbUserSession.UserId) - return api.SingleDataResponse(http.StatusOK, userSession) -} - -func (userSessionsApi *UserSessionsApi) PostUserSession(vars *api.ApiVar) api.ApiResponse { - userSession := &models.UserSession{} - - err := models.DeserializeJson(vars.RequestBody, userSession) - if err != nil { - return api.BadRequest(api.EntityFormatError) - } - - if !apifilter.CheckUserSessionIntegrity(userSession) { - return api.BadRequest(api.EntityIntegrityError) - } - - today := time.Now().Local() - userSession.ExpireDate = today.Add(time.Hour * 24 * daysUntilExpire) - - dbUserSession := userSession.Collapse() - if dbUserSession == nil { - return api.InternalServerError(api.EntityProcessError) - } - - err = userloginservice.CreateUserSession(dbUserSession) - if err != nil { - return api.InternalServerError(api.EntityProcessError) - } - userSession.Id = dbUserSession.Id - - userloginservice.DeleteExpiredSessionsForUser(dbUserSession.UserId) - return api.SingleDataResponse(http.StatusCreated, userSession) -} diff --git a/api/userloginapi/user_login_api_test.go b/api/userloginapi/user_login_api_test.go deleted file mode 100644 index 393bd72..0000000 --- a/api/userloginapi/user_login_api_test.go +++ /dev/null @@ -1,79 +0,0 @@ -package userloginapi - -import ( - "gopkg.in/mgo.v2/bson" - "gost/api" - "gost/models" - "gost/service/userloginservice" - "gost/tests" - "net/http" - "net/url" - "testing" - "time" -) - -const userSessionsRoute = "[{\"id\": \"UserSessionsRoute\", \"pattern\": \"/users/login\", \"handlers\": {\"DELETE\": \"DeleteUserSession\", \"GET\": \"GetUserSession\", \"POST\": \"PostUserSession\", \"PUT\": \"PutUserSession\"}}]" -const apiPath = "/users/login" - -type dummyUserSession struct { - BadField string -} - -func (userSession *dummyUserSession) PopConstrains() {} - -func TestUserSessionsApi(t *testing.T) { - tests.InitializeServerConfigurations(userSessionsRoute, new(UserSessionsApi)) - - testPostUserSessionInBadFormat(t) - sessionId, token := testPostUserSessionInGoodFormat(t) - testGetUserSessionWithInexistentTokenInDB(t) - testGetUserSessionWithGoodIdParam(t, token) - - userloginservice.DeleteUserSession(sessionId) -} - -func testGetUserSessionWithInexistentTokenInDB(t *testing.T) { - params := url.Values{} - params.Add("token", "asagasgsaga7615651") - - tests.PerformApiTestCall(apiPath, api.GET, http.StatusNotFound, params, nil, t) -} - -func testGetUserSessionWithGoodIdParam(t *testing.T, token string) { - params := url.Values{} - params.Add("token", token) - - rw := tests.PerformApiTestCall(apiPath, api.GET, http.StatusOK, params, nil, t) - - body := rw.Body.String() - if len(body) == 0 { - t.Error("Response body is empty or in deteriorated format:", body) - } - -} - -func testPostUserSessionInBadFormat(t *testing.T) { - dUserSession := &dummyUserSession{ - BadField: "bad value", - } - - tests.PerformApiTestCall(apiPath, api.POST, http.StatusBadRequest, nil, dUserSession, t) -} - -func testPostUserSessionInGoodFormat(t *testing.T) (bson.ObjectId, string) { - userSession := &models.UserSession{ - Id: bson.NewObjectId(), - User: models.User{Id: bson.NewObjectId()}, - Token: "as7f6as8faf5aasf6721rqf", - ExpireDate: time.Now().Local(), - } - - rw := tests.PerformApiTestCall(apiPath, api.POST, http.StatusCreated, nil, userSession, t) - - body := rw.Body.String() - if len(body) == 0 { - t.Error("Response body is empty or in deteriorated format:", body) - } - - return userSession.Id, userSession.Token -} diff --git a/app-rename b/app-rename old mode 100644 new mode 100755 diff --git a/auth/auth.go b/auth/auth.go new file mode 100755 index 0000000..edb91be --- /dev/null +++ b/auth/auth.go @@ -0,0 +1,135 @@ +package auth + +import ( + "errors" + "gost/auth/cookies" + "gost/auth/identity" + "gost/security" + "gost/util" + "net/http" + "strings" + + "gopkg.in/mgo.v2/bson" +) + +// The keys that are used in the request header to authorize the user +const ( + AuthorizationHeader = "Authorization" + AuthorizationScheme = "GHOST-TOKEN" +) + +// Errors generated by the auth package +var ( + ErrInvalidScheme = errors.New("The used authorization scheme is invalid or not supported") + ErrInvalidGhostToken = errors.New("The given token is expired or invalid") + ErrInvalidUser = errors.New("There is no application user with the given ID") + ErrDeactivatedUser = errors.New("The current user account is deactivated or inexistent") + ErrInexistentClientDetails = errors.New("Missing client details. Cannot create authorization for anonymous client") + + errAnonymousUser = errors.New("The user has no identity") +) + +// GenerateUserAuth generates a new gost-token, saves it in the database and returns it to the client +func GenerateUserAuth(userID bson.ObjectId, client *cookies.Client) (string, error) { + if client == nil { + return ErrInexistentClientDetails.Error(), ErrInexistentClientDetails + } + + var user *identity.ApplicationUser + var isUserExistent bool + if user, isUserExistent = identity.IsUserExistent(userID); !isUserExistent { + return ErrInvalidUser.Error(), ErrInvalidUser + } + + session, err := cookies.NewSession(userID, user.AccountType, client) + if err != nil { + return err.Error(), err + } + + err = session.Save() + if err != nil { + return err.Error(), err + } + + ghostToken, err := generateGhostToken(session) + + return ghostToken, err +} + +// Authorize tries to authorize an existing gostToken +func Authorize(httpHeader http.Header) (*identity.Identity, error) { + ghostToken, err := extractGhostToken(httpHeader) + if err != nil { + if err == errAnonymousUser { + return identity.NewAnonymous(), nil + } + + return nil, err + } + + encryptedToken, err := util.Decode([]byte(ghostToken)) + if err != nil { + return nil, err + } + + jsonToken, err := security.Decrypt(encryptedToken) + if err != nil { + return nil, err + } + + cookie := new(cookies.Session) + err = util.DeserializeJSON(jsonToken, cookie) + if err != nil { + return nil, err + } + + dbCookie, err := cookies.GetSession(cookie.Token) + if err != nil || dbCookie == nil { + return nil, ErrDeactivatedUser + } + + if !identity.IsUserActivated(dbCookie.UserID) { + return nil, ErrDeactivatedUser + } + + go dbCookie.ResetToken() + + return identity.New(dbCookie), nil +} + +func generateGhostToken(session *cookies.Session) (string, error) { + jsonToken, err := util.SerializeJSON(session) + if err != nil { + return err.Error(), err + } + + encryptedToken, err := security.Encrypt(jsonToken) + if err != nil { + return err.Error(), err + } + + ghostToken := util.Encode(encryptedToken) + + return string(ghostToken), nil +} + +func extractGhostToken(httpHeader http.Header) (string, error) { + var gostToken string + + if gostToken = httpHeader.Get(AuthorizationHeader); len(gostToken) == 0 { + return errAnonymousUser.Error(), errAnonymousUser + } + + if !strings.Contains(gostToken, AuthorizationScheme) { + return ErrInvalidScheme.Error(), ErrInvalidScheme + } + + gostTokenValue := strings.TrimPrefix(gostToken, AuthorizationScheme) + gostTokenValue = strings.TrimSpace(gostTokenValue) + + if len(gostTokenValue) == 0 { + return ErrInvalidGhostToken.Error(), ErrInvalidGhostToken + } + + return gostTokenValue, nil +} diff --git a/auth/cookies/dbstore.go b/auth/cookies/dbstore.go new file mode 100755 index 0000000..f85d4fa --- /dev/null +++ b/auth/cookies/dbstore.go @@ -0,0 +1,103 @@ +package cookies + +import ( + "gost/service" + + "gopkg.in/mgo.v2" + "gopkg.in/mgo.v2/bson" +) + +// DatabaseCookieStore manages cookies using a database +type DatabaseCookieStore struct { + location string +} + +// ReadCookie fetches a cookie from the cookie store +func (store *DatabaseCookieStore) ReadCookie(key string) (*Session, error) { + session, collection := service.Connect(store.location) + defer session.Close() + + cookie := Session{} + err := collection.Find(bson.M{"token": key}).One(&cookie) + + return &cookie, err +} + +// WriteCookie writes a cookie in the cookie store. If that cookie already exists, +// it is overwritten +func (store *DatabaseCookieStore) WriteCookie(cookie *Session) error { + session, collection := service.Connect(store.location) + defer session.Close() + + err := collection.UpdateId(cookie.ID, cookie) + if err == mgo.ErrNotFound { + err = collection.Insert(cookie) + } + + return err +} + +// DeleteCookie deletes a cookie from the cookie storage +func (store *DatabaseCookieStore) DeleteCookie(cookie *Session) error { + session, collection := service.Connect(store.location) + defer session.Close() + + err := collection.Remove(bson.M{"token": cookie.Token}) + return err +} + +// GetAllUserCookies returns all the cookies that a certain user has +func (store *DatabaseCookieStore) GetAllUserCookies(userID bson.ObjectId) ([]*Session, error) { + session, collection := service.Connect(store.location) + defer session.Close() + + var userSessions []*Session + err := collection.Find(bson.M{"userID": userID}).All(&userSessions) + + return userSessions, err +} + +// Init initializes the cookie store +func (store *DatabaseCookieStore) Init() { + session, collection := service.Connect(store.location) + defer session.Close() + session.SetMode(mgo.Monotonic, true) + + if store.hasTokenIndex(collection) { + return + } + + index := mgo.Index{ + Key: []string{"$text:token"}, + } + + err := collection.EnsureIndex(index) + if err != nil { + panic(err) + } +} + +// hasTokenIndex verifies if there is already an index created for the token field of a collection +func (store *DatabaseCookieStore) hasTokenIndex(collection *mgo.Collection) bool { + indexes, err := collection.Indexes() + if err != nil { + panic(ErrInitializationFailed) + } + + for _, index := range indexes { + for _, key := range index.Key { + if key == "token" { + return true + } + } + } + + return false +} + +// NewDatabaseCookieStore creates a new DatabaseCookieStore pointer entity +func NewDatabaseCookieStore(storeLocation string) *DatabaseCookieStore { + store := &DatabaseCookieStore{location: storeLocation} + + return store +} diff --git a/auth/cookies/filestore.go b/auth/cookies/filestore.go new file mode 100755 index 0000000..bf1e77e --- /dev/null +++ b/auth/cookies/filestore.go @@ -0,0 +1,192 @@ +package cookies + +import ( + "bytes" + "errors" + "gost/security" + "gost/util" + "io/ioutil" + "os" + + "gopkg.in/mgo.v2/bson" +) + +// FileCookieStore manages cookies using files on the local drive +type FileCookieStore struct { + location string +} + +// ReadCookie fetches a cookie from the cookie store +func (store *FileCookieStore) ReadCookie(key string) (*Session, error) { + fileName := fileLocation(store.location, key) + encryptedData, err := ioutil.ReadFile(fileName) + if err != nil { + return nil, err + } + + jsonData, err := security.Decrypt(encryptedData) + if err != nil { + return nil, err + } + + var session *Session + err = util.DeserializeJSON(jsonData, session) + + return session, err +} + +// WriteCookie writes a cookie in the cookie store. If that cookie already exists, +// it is overwritten +func (store *FileCookieStore) WriteCookie(cookie *Session) error { + fileName := fileLocation(store.location, cookie.Token) + jsonData, err := util.SerializeJSON(cookie) + if err != nil { + return err + } + + encryptedData, err := security.Encrypt(jsonData) + if err != nil { + return err + } + + err = addUserToken(store.location, cookie.UserID, cookie.Token) + if err != nil { + return err + } + + return ioutil.WriteFile(fileName, encryptedData, os.ModeDevice) +} + +// DeleteCookie deletes a cookie from the cookie storage +func (store *FileCookieStore) DeleteCookie(cookie *Session) error { + fileName := fileLocation(store.location, cookie.Token) + + err := removeUserToken(store.location, cookie.UserID, cookie.Token) + if err != nil { + return err + } + + return os.Remove(fileName) +} + +// GetAllUserCookies returns all the cookies that a certain user has +func (store *FileCookieStore) GetAllUserCookies(userID bson.ObjectId) ([]*Session, error) { + userTokens, err := getUserTokens(store.location, userID) + if err != nil { + return nil, err + } + + var cookies []*Session + for _, token := range userTokens { + session, err := store.ReadCookie(token) + if err != nil { + return nil, err + } + + cookies = append(cookies, session) + } + + return cookies, err +} + +// Init initializes the cookie store +func (store *FileCookieStore) Init() { + if _, err := os.Stat(store.location); os.IsNotExist(err) { + err = os.Mkdir(store.location, os.ModeDevice) + + if err != nil { + panic(ErrInitializationFailed) + } + } +} + +// cookieLocation computes and returns the location of a certain cookie +func fileLocation(storeLocation, key string) string { + var buffer bytes.Buffer + + buffer.WriteString(storeLocation) + buffer.WriteRune('/') + buffer.WriteString(key) + + return buffer.String() +} + +// NewFileCookieStore creates a new NewFileCookieStore pointer entity +func NewFileCookieStore(storeLocation string) *FileCookieStore { + store := &FileCookieStore{location: storeLocation} + + return store +} + +func addUserToken(storeLocation string, userID bson.ObjectId, token string) error { + userTokens, err := getUserTokens(storeLocation, userID) + if err != nil { + return err + } + + var isAlreadyAdded bool + for _, userToken := range userTokens { + if userToken == token { + isAlreadyAdded = true + break + } + } + + if !isAlreadyAdded { + userTokens = append(userTokens, token) + } + + return saveUserTokens(storeLocation, userID, userTokens) +} + +func removeUserToken(storeLocation string, userID bson.ObjectId, token string) error { + userTokens, err := getUserTokens(storeLocation, userID) + if err != nil { + return err + } + + tokenIndex := -1 + for index, userToken := range userTokens { + if userToken == token { + tokenIndex = index + break + } + } + + if tokenIndex == -1 { + return errors.New("User token does not exist") + } + + userTokens = append(userTokens[:tokenIndex], userTokens[tokenIndex+1:]...) + + return saveUserTokens(storeLocation, userID, userTokens) +} + +func getUserTokens(storeLocation string, userID bson.ObjectId) ([]string, error) { + userIndexFile := fileLocation(storeLocation, userID.Hex()) + + fileContent, err := ioutil.ReadFile(userIndexFile) + if err != nil { + return nil, err + } + + var userTokens []string + err = util.DeserializeJSON(fileContent, userTokens) + if err != nil { + return nil, err + } + + return userTokens, nil +} + +func saveUserTokens(storeLocation string, userID bson.ObjectId, tokens []string) error { + userIndexFile := fileLocation(storeLocation, userID.Hex()) + + jsonData, err := util.SerializeJSON(tokens) + if err != nil { + return err + } + + err = ioutil.WriteFile(userIndexFile, jsonData, os.ModeDevice) + return err +} diff --git a/auth/cookies/sessions.go b/auth/cookies/sessions.go new file mode 100755 index 0000000..067f7e3 --- /dev/null +++ b/auth/cookies/sessions.go @@ -0,0 +1,140 @@ +package cookies + +import ( + "errors" + "gost/util" + "time" + + "gopkg.in/mgo.v2/bson" +) + +const ( + defaultTokenExpireTime = 24 * 7 * time.Hour +) + +var ( + tokenExpireTime = defaultTokenExpireTime +) + +// Errors generated during session handling +var ( + ErrTokenExpired = errors.New("The session token has expired") +) + +// Session is a struct representing the session that a user has. +// Sessions are active since login until they expire or the user disconnects +type Session struct { + ID bson.ObjectId `bson:"_id" json:"-"` + UserID bson.ObjectId `bson:"userID,omitempty" json:"userID"` + Token string `bson:"token,omitempty" json:"token"` + AccountType int `bson:"accountType,omitempty" json:"accountType"` + ExpireTime time.Time `bson:"expireTime,omitempty" json:"-"` + Client *Client `bson:"client,omitempty" json:"client"` +} + +// Client struct contains information regarding the client that has made the http request +type Client struct { + IPAddress string `bson:"ipAddress,omitempty" json:"ipAddress"` + Browser string `bson:"browser,omitempty" json:"browser"` + OS string `bson:"os,omitempty" json:"os"` + Country string `bson:"country,omitempty" json:"country"` + State string `bson:"state,omitempty" json:"state"` + City string `bson:"city,omitempty" json:"city"` + Latitude float64 `bson:"latitude,omitempty" json:"latitude"` + Longitude float64 `bson:"longitude,omitempty" json:"longitude"` +} + +// Save saves the session in the cookie store +func (session *Session) Save() error { + return cookieStore.WriteCookie(session) +} + +// Delete deletes the session from the cookie store +func (session *Session) Delete() error { + return cookieStore.DeleteCookie(session) +} + +// IsExpired returns true if the session has expired +func (session *Session) IsExpired() bool { + return util.IsDateExpiredFromNow(session.ExpireTime) +} + +// IsUserInRole verifies if the user with the current session has a specific role +func (session *Session) IsUserInRole(role int) bool { + return session.AccountType == role +} + +// ResetToken generates a new token and resets the expire time target of the session +// This also triggers a Save() action, to update the cookie store +func (session *Session) ResetToken() error { + session.ExpireTime = util.NextDateFromNow(tokenExpireTime) + + return session.Save() +} + +// NewSession generates a new Session pointer that contains the given userID and +// a unique token used as an identifier +func NewSession(userID bson.ObjectId, accountType int, client *Client) (*Session, error) { + token, err := util.GenerateUUID() + if err != nil { + return nil, err + } + + session := &Session{ + ID: bson.NewObjectId(), + UserID: userID, + AccountType: accountType, + Token: token, + ExpireTime: util.NextDateFromNow(tokenExpireTime), + Client: client, + } + + return session, nil +} + +// GetSession retrieves a session from the cookie store +func GetSession(token string) (*Session, error) { + session, err := cookieStore.ReadCookie(token) + if err != nil { + return nil, err + } + + if session.IsExpired() { + err = session.Delete() + if err == nil { + err = ErrTokenExpired + } + + return nil, err + } + + return session, nil +} + +// GetUserSessions retrieves all the sessions that a user has +func GetUserSessions(userID bson.ObjectId) ([]*Session, error) { + sessions, err := cookieStore.GetAllUserCookies(userID) + if err != nil { + return nil, err + } + + for i := 0; i < len(sessions); { + if sessions[i].IsExpired() { + err = sessions[i].Delete() + if err == nil { + err = ErrTokenExpired + } + + sessions = append(sessions[:i], sessions[i+1:]...) + } else { + i++ + } + } + + return sessions, nil +} + +// ConfigureTokenExpireTime is used to change the default cofiguration of token expiration times +func ConfigureTokenExpireTime(expireTime time.Duration) { + tokenExpireTime = expireTime +} diff --git a/auth/cookies/store.go b/auth/cookies/store.go new file mode 100755 index 0000000..f406270 --- /dev/null +++ b/auth/cookies/store.go @@ -0,0 +1,43 @@ +// Package cookies uses the DatabaseCookieStore type as its default cookie store +// for managing user sessions +package cookies + +import ( + "errors" + + "gopkg.in/mgo.v2/bson" +) + +// Errors generated by the cookie stores +var ( + ErrInitializationFailed = errors.New("Initialization of the cookie store has failed") +) + +const ( + defaultCookieStoreLocation = "cookies" +) + +// CookieStore is an interface used for describing entities that can manage +// the location of user sessions (cookies) and performs operations such as +// reading or writing cookie from/to that location +type CookieStore interface { + ReadCookie(key string) (*Session, error) + WriteCookie(cookie *Session) error + DeleteCookie(cookie *Session) error + GetAllUserCookies(userID bson.ObjectId) ([]*Session, error) + Init() +} + +var defaultCookieStore = &DatabaseCookieStore{location: defaultCookieStoreLocation} +var cookieStore CookieStore = defaultCookieStore + +// SetCookieStore sets the cookie store that will be used by the system. +// If this method is not called, the default cookie store will be used +func SetCookieStore(store CookieStore) { + cookieStore = store +} + +// InitCookieStore initializes the currently active cookie store +func InitCookieStore() { + cookieStore.Init() +} diff --git a/auth/identity/identity.go b/auth/identity/identity.go new file mode 100755 index 0000000..d4d8d9b --- /dev/null +++ b/auth/identity/identity.go @@ -0,0 +1,40 @@ +package identity + +import "gost/auth/cookies" + +// Identity represents the identity of the user the is in the current context +type Identity struct { + Session *cookies.Session + isAuthorized bool +} + +// IsAnonymous returns true if the current user is anonymous +func (identity *Identity) IsAnonymous() bool { + return !identity.isAuthorized +} + +// IsAuthorized returns true if the user is authorized +func (identity *Identity) IsAuthorized() bool { + return identity.isAuthorized +} + +// IsAdmin returns true if the current authorized user is in the admin role +func (identity *Identity) IsAdmin() bool { + return identity.IsAuthorized() && identity.Session.IsUserInRole(AccountTypeAdministrator) +} + +// New creates a new Identity based on a user session +func New(session *cookies.Session) *Identity { + return &Identity{ + Session: session, + isAuthorized: session != nil, + } +} + +// NewAnonymous creates a new anonymous Identity, with no user session defined +func NewAnonymous() *Identity { + return &Identity{ + Session: nil, + isAuthorized: false, + } +} diff --git a/auth/identity/user.go b/auth/identity/user.go new file mode 100755 index 0000000..9e8e566 --- /dev/null +++ b/auth/identity/user.go @@ -0,0 +1,130 @@ +package identity + +import ( + "gost/service" + "time" + + "gopkg.in/mgo.v2/bson" +) + +// Constants describing the type of account the the users have +const ( + AccountTypeNormalUser = iota + AccountTypeAdministrator = iota +) + +// Constants describing the status of the current user account +const ( + AccountStatusDeactivated = iota + AccountStatusActivated = iota +) + +const collectionName = "appusers" + +// ApplicationUser contains information necessary for managing accounts +type ApplicationUser struct { + ID bson.ObjectId `bson:"_id" json:"id"` + + Email string `bson:"email,omitempty" json:"email"` + Password string `bson:"password,omitempty" json:"password"` + AccountType int `bson:"accountType,omitempty" json:"accountType"` + ResetPasswordToken string `bson:"resetPasswordToken,omitempty" json:"resetPasswordToken"` + ResetPasswordTokenExpireDate time.Time `bson:"resetPasswordTokenExpireDate,omitempty" json:"resetPasswordTokenExpireDate"` + ActivateAccountToken string `bson:"activateAccountToken" json:"activateAccountToken"` + ActivateAccountTokenExpireDate time.Time `bson:"activateAccountTokenExpireDate,omitempty" json:"activateAccountTokenExpireDate"` + AccountStatus int `bson:"accountStatus,omitempty" json:"accountStatus"` +} + +// CreateUser adds a new ApplicationUser to the database +func CreateUser(user *ApplicationUser) error { + session, collection := service.Connect(collectionName) + defer session.Close() + + if user.ID == "" { + user.ID = bson.NewObjectId() + } + + var err = collection.Insert(user) + + return err +} + +// UpdateUser updates an existing ApplicationUser in the database +func UpdateUser(user *ApplicationUser) error { + session, collection := service.Connect(collectionName) + defer session.Close() + + if user.ID == "" { + return service.ErrNoIDSpecified + } + + var err = collection.UpdateId(user.ID, user) + + return err +} + +// GetUser retrieves an ApplicationUser from the database, based on its ID +func GetUser(userID bson.ObjectId) (*ApplicationUser, error) { + session, collection := service.Connect(collectionName) + defer session.Close() + + var user = ApplicationUser{} + var err = collection.FindId(userID).One(&user) + + return &user, err +} + +// GetUserByActivationToken retrieves an ApplicationUser from the database, based on its account activation token +func GetUserByActivationToken(token string) (*ApplicationUser, error) { + session, collection := service.Connect(collectionName) + defer session.Close() + + var user = ApplicationUser{} + var err = collection.Find(bson.M{"activateAccountToken": token}).One(&user) + + return &user, err +} + +// GetUserByResetPasswordToken retrieves an ApplicationUser from the database, based on its reset password token +func GetUserByResetPasswordToken(token string) (*ApplicationUser, error) { + session, collection := service.Connect(collectionName) + defer session.Close() + + var user = ApplicationUser{} + var err = collection.Find(bson.M{"resetPasswordToken": token}).One(&user) + + return &user, err +} + +// GetUserByEmail retrieves an ApplicationUser from the database, based on its email address +func GetUserByEmail(emailAddress string) (*ApplicationUser, error) { + session, collection := service.Connect(collectionName) + defer session.Close() + + var user = ApplicationUser{} + var err = collection.Find(bson.M{"email": emailAddress}).One(&user) + + return &user, err +} + +// DeleteUser deletes an ApplicationUser from the database, based on its ID +func DeleteUser(userID bson.ObjectId) error { + session, collection := service.Connect(collectionName) + defer session.Close() + + return collection.RemoveId(userID) +} + +// IsUserExistent verifies if an user with the given id exists +func IsUserExistent(userID bson.ObjectId) (*ApplicationUser, bool) { + var user, err = GetUser(userID) + + return user, err == nil && user != nil +} + +// IsUserActivated verifies if an user account is activated +func IsUserActivated(userID bson.ObjectId) bool { + var user, err = GetUser(userID) + + return err == nil && user.AccountStatus == AccountStatusActivated +} diff --git a/auth/identity/user_test.go b/auth/identity/user_test.go new file mode 100755 index 0000000..92737c6 --- /dev/null +++ b/auth/identity/user_test.go @@ -0,0 +1,101 @@ +package identity + +import ( + "gost/service" + testconfig "gost/tests/config" + "gost/util" + "testing" + "time" + + "gopkg.in/mgo.v2/bson" +) + +func TestUserCRUD(t *testing.T) { + user := &ApplicationUser{} + + setUpUsersTest(t) + defer tearDownUsersTest(t, user) + + createUser(t, user) + verifyUserCorresponds(t, user) + + if !t.Failed() { + changeAndUpdateUser(t, user) + verifyUserCorresponds(t, user) + } +} + +func setUpUsersTest(t *testing.T) { + testconfig.InitTestsDatabase() + service.InitDbService() + + if recover() != nil { + t.Fatal("Test setup failed!") + } +} + +func tearDownUsersTest(t *testing.T, user *ApplicationUser) { + err := deleteUser(user.ID) + + if err != nil { + t.Fatal("The user document could not be deleted!") + } +} + +func createUser(t *testing.T, user *ApplicationUser) { + *user = ApplicationUser{ + ID: bson.NewObjectId(), + Password: "CoddoPass", + AccountType: AccountTypeAdministrator, + Email: "test@tests.com", + ResetPasswordToken: "as7f6as8faf5aasf6721rqf", + ResetPasswordTokenExpireDate: time.Now(), + AccountStatus: AccountStatusActivated, + } + + err := CreateUser(user) + + if err != nil { + t.Fatal("The user document could not be created!") + } +} + +func changeAndUpdateUser(t *testing.T, user *ApplicationUser) { + user.Email = "testEmailCHanged@email.go" + user.Password = "ChangedPassword" + user.AccountType = AccountTypeNormalUser + user.AccountStatus = AccountStatusDeactivated + + err := UpdateUser(user) + + if err != nil { + t.Fatal("The user document could not be updated!") + } +} + +func verifyUserCorresponds(t *testing.T, user *ApplicationUser) { + dbuser, err := GetUser(user.ID) + + if err != nil || dbuser == nil { + t.Error("Could not fetch the user document from the database!") + } + + if user.AccountStatus != dbuser.AccountStatus || user.AccountType != dbuser.AccountType || + user.ActivateAccountToken != dbuser.ActivateAccountToken || + !util.CompareDates(user.ActivateAccountTokenExpireDate, dbuser.ActivateAccountTokenExpireDate) || + user.Email != dbuser.Email || user.Password != dbuser.Password || + user.ResetPasswordToken != dbuser.ResetPasswordToken || + !util.CompareDates(user.ResetPasswordTokenExpireDate, dbuser.ResetPasswordTokenExpireDate) { + + t.Error("The user document doesn't correspond with the document extracted from the database!") + } +} + +func deleteUser(userID bson.ObjectId) error { + session, collection := service.Connect(collectionName) + defer session.Close() + + err := collection.RemoveId(userID) + + return err +} diff --git a/auth/users.go b/auth/users.go new file mode 100755 index 0000000..cb50257 --- /dev/null +++ b/auth/users.go @@ -0,0 +1,163 @@ +package auth + +import ( + "errors" + "fmt" + "gost/auth/identity" + "gost/email" + "gost/util" + "log" + "time" + + "gopkg.in/mgo.v2/bson" +) + +const ( + passwordResetTokenExpireTime = 24 * time.Hour + accountActivationTokenExpireTime = 7 * 24 * time.Hour +) + +// Errors that can occur during ApplicationUser management +var ( + ErrActivationTokenExpired = errors.New("The activation token has expired") + ErrResetPasswordTokenExpired = errors.New("The reset password token has expired") +) + +// CreateAppUser creates a new ApplicationUser with the given data, generates an activation token +// and sends an email containing a link used for activating the account +func CreateAppUser(emailAddress, password string, accountType int, activationServiceLink string) (*identity.ApplicationUser, error) { + var token, err = util.GenerateUUID() + if err != nil { + return nil, err + } + + passwordHash, err := util.HashString(password) + if err != nil { + return nil, err + } + + var user = &identity.ApplicationUser{ + ID: bson.NewObjectId(), + Email: emailAddress, + Password: passwordHash, + AccountType: accountType, + ActivateAccountToken: token, + ActivateAccountTokenExpireDate: util.NextDateFromNow(accountActivationTokenExpireTime), + } + + err = identity.CreateUser(user) + if err != nil { + return nil, err + } + + go sendAccountActivationEmail(emailAddress, activationServiceLink, token) + + return user, nil +} + +// ActivateAppUser activates an application user based on its token +func ActivateAppUser(token string) error { + var user, err = identity.GetUserByActivationToken(token) + if err != nil { + return err + } + + if util.IsDateExpiredFromNow(user.ActivateAccountTokenExpireDate) { + return ErrActivationTokenExpired + } + + user.AccountStatus = identity.AccountStatusActivated + + return identity.UpdateUser(user) +} + +// ResetPassword resets the password of an application user +func ResetPassword(token, password string) error { + var user, err = identity.GetUserByResetPasswordToken(token) + if err != nil { + return err + } + + if util.IsDateExpiredFromNow(user.ResetPasswordTokenExpireDate) { + return ErrResetPasswordTokenExpired + } + + passwordHash, err := util.HashString(password) + if err != nil { + return err + } + + user.Password = passwordHash + + return identity.UpdateUser(user) +} + +// RequestResetPassword generates a reset token and sends an email with the link where to perform the change +func RequestResetPassword(emailAddress, passwordResetServiceLink string) error { + var user, err = identity.GetUserByEmail(emailAddress) + if err != nil { + return err + } + + token, err := util.GenerateUUID() + if err != nil { + return err + } + + user.ResetPasswordToken = token + user.ResetPasswordTokenExpireDate = util.NextDateFromNow(passwordResetTokenExpireTime) + + err = identity.UpdateUser(user) + if err != nil { + return err + } + + go sendPasswordResetEmail(emailAddress, passwordResetServiceLink, token) + + return nil +} + +// ResendAccountActivationEmail resends the email with the details for activating their user account +func ResendAccountActivationEmail(emailAddress, activationServiceLink string) error { + var user, err = identity.GetUserByEmail(emailAddress) + if err != nil { + return err + } + + token, err := util.GenerateUUID() + if err != nil { + return err + } + + user.ActivateAccountToken = token + user.ActivateAccountTokenExpireDate = util.NextDateFromNow(accountActivationTokenExpireTime) + + err = identity.UpdateUser(user) + if err != nil { + return err + } + + go sendAccountActivationEmail(emailAddress, activationServiceLink, token) + + return nil +} + +func sendAccountActivationEmail(userEmail, activationServiceLink, token string) { + var accountActivationLink = fmt.Sprintf(activationServiceLink, token) + + err := email.SendAccountActivationEmail(userEmail, accountActivationLink) + + if err != nil { + log.Printf(fmt.Sprintf("Error in sending account activation email to: %s", userEmail)) + } +} + +func sendPasswordResetEmail(userEmail, passwordResetServiceLink, token string) { + var passwordResetLink = fmt.Sprintf(passwordResetServiceLink, token) + + err := email.SendPasswordResetEmail(userEmail, passwordResetLink) + + if err != nil { + log.Printf(fmt.Sprintf("Error in sending password reset email to: %s", userEmail)) + } +} diff --git a/bll/transactions.go b/bll/transactions.go new file mode 100755 index 0000000..cd8932c --- /dev/null +++ b/bll/transactions.go @@ -0,0 +1,44 @@ +package bll + +import ( + "gost/api" + "gost/filter/apifilter" + "gost/orm/models" + "gost/service/transactionservice" + "net/http" + + "gopkg.in/mgo.v2/bson" +) + +// GetTransaction retrieves an existing Transaction based on its ID +func GetTransaction(transactionID bson.ObjectId) api.Response { + dbTransaction, err := transactionservice.GetTransaction(transactionID) + if err != nil || dbTransaction == nil { + return api.NotFound(api.ErrEntityNotFound) + } + + transaction := &models.Transaction{} + transaction.Expand(dbTransaction) + + return api.JSONResponse(http.StatusOK, transaction) +} + +// CreateTransaction creates a new Transaction +func CreateTransaction(transaction *models.Transaction) api.Response { + if !apifilter.CheckTransactionIntegrity(transaction) { + return api.BadRequest(api.ErrEntityIntegrity) + } + + dbTransaction := transaction.Collapse() + if dbTransaction == nil { + return api.InternalServerError(api.ErrEntityProcessing) + } + + err := transactionservice.CreateTransaction(dbTransaction) + if err != nil { + return api.InternalServerError(api.ErrEntityProcessing) + } + transaction.ID = dbTransaction.ID + + return api.JSONResponse(http.StatusCreated, transaction) +} diff --git a/cache/cache.go b/cache/cache.go old mode 100644 new mode 100755 index 09c0c4b..4a7f242 --- a/cache/cache.go +++ b/cache/cache.go @@ -1,23 +1,57 @@ package cache import ( - "bytes" - "net/url" + "errors" + "gost/util" "time" ) const ( - STATUS_ON = true - STATUS_OFF = false + // StatusON shows that the cashing system is up and running + StatusON = true + // StatusOFF shows that the caching system is stopped and not functional + StatusOFF = false ) const ( - CACHE_EXPIRE_TIME = 1 * time.Minute + // DefaultCacheExpireTime represents the maximum duration that an item can stay cached + DefaultCacheExpireTime = 7 * 24 * time.Hour ) -var Status bool = STATUS_OFF -var selectedCacheExpireTime time.Duration +var ( + // ErrKeyInvalidated is used when a search key is inexistent or has been invalidated + ErrKeyInvalidated = errors.New("The search key has been invalidated") + + // ErrKeyFormat is used when the format of the key is wrong or cannot be parsed + ErrKeyFormat = errors.New("The search key is not in a correct format") + + // ErrCachingSystemStopped is used to indicate that the caching system is not available or stopped + ErrCachingSystemStopped = errors.New("The search key has been invalidated") +) + +var ( + // Status represents the current status of the caching system + Status = StatusOFF + + selectedCacheExpireTime time.Duration +) + +var memoryCache = make(map[string]map[string]*Cache) + +var ( + getKeyChannel = make(chan *combinedKey) + getChan = make(chan chan *Cache) + errorChan = make(chan error) + cacheChan = make(chan *Cache) + invalidateChan = make(chan string) + invalidateExpiredChan = make(chan *combinedKey) + exitChan = make(chan int) +) +// Cacher is the interface that has all the basic methods used by cached items +// +// Each cache item can either: become Cached, become Invalidated (triggered or expired), +// or have its expire time reset type Cacher interface { Cache() Invalidate() @@ -25,8 +59,16 @@ type Cacher interface { ResetExpireTime() } +type combinedKey struct { + Key string + DataKey string +} + +// A Cache entity is used to store precise information in the memory cache +// using a key (unique idenfier) and its actual data type Cache struct { - Query string + Key string + DataKey string Data []byte StatusCode int ContentType string @@ -34,82 +76,138 @@ type Cache struct { ExpireTime time.Time } +// Cache caches the current entity into memory func (cache *Cache) Cache() { - cache.ResetExpireTime() - cacheChan <- cache + go func() { + cacheChan <- cache + }() } +// Invalidate invalidates the current entity by removing it from the cache func (cache *Cache) Invalidate() { - invalidateChan <- cache.Query + go func() { + invalidateChan <- cache.Key + }() } +// InvalidateIfExpired checks whether the active time of the current entity +// has expired, and if it did, it invalidates it func (cache *Cache) InvalidateIfExpired(limit time.Time) { - if cache.ExpireTime.Before(limit) { - cache.Invalidate() + if !util.IsDateExpired(cache.ExpireTime, limit) { + return } -} -func (cache *Cache) ResetExpireTime() { - cache.ExpireTime = time.Now().Add(selectedCacheExpireTime) + go func() { + invalidateExpiredChan <- &combinedKey{cache.Key, cache.DataKey} + }() } -func QueryByKey(key string) *Cache { +// ResetExpireTime resets the timer for when the current entity will expire +func (cache *Cache) ResetExpireTime() { go func() { - getKeyChannel <- key + cache.ExpireTime = util.NextDateFromNow(selectedCacheExpireTime) }() +} - flag := make(chan *Cache) - defer close(flag) - - getChan <- flag +// StartCachingSystem starts the caching system. The status is set to StatusON. +// +// The following async loops are started: +// - Selection loop for the request channels +// - Invalidator loop which makes sure that the entities will not be stored in the cache forever +func StartCachingSystem(cacheExpireTime time.Duration) { + Status = StatusON - return <-flag -} + selectedCacheExpireTime = cacheExpireTime -func QueryByRequest(form url.Values, endpoint string) *Cache { - return QueryByKey(MapKey(form, endpoint)) + go startCachingLoop() + go startExpiredInvalidator(cacheExpireTime) } -func MapKey(form url.Values, endpoint string) string { - var buf bytes.Buffer +// StopCachingSystem stops the caching system/ The status is set to StatusOFF. +// All the async loops are stopped and the channels (except the errors channel) are closed. +func StopCachingSystem() { + Status = StatusOFF - buf.WriteString(endpoint) - buf.WriteRune(':') - buf.WriteString(form.Encode()) + stopCachingSystem() - return buf.String() + go func() { + exitChan <- 1 + close(exitChan) + }() } -var memoryCache = make(map[string]*Cache) +// Query searches for a certain storage key in the memory cache +// and returns the found Cache item based on its data key. +// An error is returned if it is inexistent or there was a problem with the search +func Query(key, dataKey string) (*Cache, error) { + if Status == StatusOFF { + return nil, ErrCachingSystemStopped + } -var ( - getKeyChannel = make(chan string) - getChan = make(chan chan *Cache) - cacheChan = make(chan *Cache) - invalidateChan = make(chan string) - exitChan = make(chan int) -) + go func() { + getKeyChannel <- &combinedKey{Key: key, DataKey: dataKey} + }() -var exited bool = false + flagChan := make(chan *Cache) + defer close(flagChan) -func stopCachingSystem() { - exited = true + go func() { + getChan <- flagChan + }() + for { + select { + case returnItem := <-flagChan: + return returnItem, nil + case err := <-errorChan: + return nil, err + default: + if Status == StatusOFF { + return nil, ErrCachingSystemStopped + } + } + } +} + +func stopCachingSystem() { close(getKeyChannel) close(getChan) close(cacheChan) close(invalidateChan) + close(invalidateExpiredChan) } func invalidate(key string) { - delete(memoryCache, key) + if _, exists := memoryCache[key]; exists { + delete(memoryCache, key) + } +} + +func invalidateExpired(key *combinedKey) { + if dataCaches, exists := memoryCache[key.Key]; exists { + if _, exists := dataCaches[key.DataKey]; exists { + delete(dataCaches, key.DataKey) + memoryCache[key.Key] = dataCaches + } + } } func storeOrUpdate(cache *Cache) { - memoryCache[cache.Query] = cache + cache.ResetExpireTime() + + var cachePoint = map[string]*Cache{} + if entryPoint, exists := memoryCache[cache.Key]; exists { + cachePoint = entryPoint + } + + cachePoint[cache.DataKey] = cache + + memoryCache[cache.Key] = cachePoint } func startCachingLoop() { + defer recoverFromCachingErrors() + Loop: for { select { @@ -117,50 +215,60 @@ Loop: break Loop case key := <-invalidateChan: invalidate(key) + case key := <-invalidateExpiredChan: + invalidateExpired(key) case cache := <-cacheChan: storeOrUpdate(cache) case flag := <-getChan: key := <-getKeyChannel - item := memoryCache[key] - if item != nil { - item.ResetExpireTime() + if len(key.Key) == 0 || len(key.DataKey) == 0 { + errorChan <- ErrKeyFormat } - flag <- item + var itemFound bool + if dataCaches, isContainerPresent := memoryCache[key.Key]; isContainerPresent { + if item, isCachePresent := dataCaches[key.DataKey]; isCachePresent { + itemFound = true + + item.ResetExpireTime() + flag <- item + } + } + + if !itemFound { + errorChan <- ErrKeyInvalidated + } } } } func startExpiredInvalidator(cacheExpireTime time.Duration) { - for !exited { - time.Sleep(cacheExpireTime) + defer recoverFromInvalidatorErrors(cacheExpireTime) - if !exited { - m := memoryCache - date := time.Now() + for Status == StatusON { + var date = util.Now() + var cacheCopy = memoryCache - for _, item := range m { + for _, dataCaches := range cacheCopy { + for _, item := range dataCaches { item.InvalidateIfExpired(date) } } + + time.Sleep(cacheExpireTime) } } -func StartCachingSystem(cacheExpireTime time.Duration) { - selectedCacheExpireTime = cacheExpireTime - - go startCachingLoop() - go startExpiredInvalidator(cacheExpireTime) - - Status = STATUS_ON +// In case of error (caching is still on), restart the entire system +func recoverFromCachingErrors() { + if r := recover(); r != nil && Status == StatusON { + go startCachingLoop() + } } -func StopCachingSystem() { - stopCachingSystem() - - go func() { - exitChan <- 1 - close(exitChan) - }() +func recoverFromInvalidatorErrors(cacheExpireTime time.Duration) { + if r := recover(); r != nil && Status == StatusON { + go startExpiredInvalidator(cacheExpireTime) + } } diff --git a/cache/cache_test.go b/cache/cache_test.go old mode 100644 new mode 100755 index 5ad7a11..e769209 --- a/cache/cache_test.go +++ b/cache/cache_test.go @@ -2,76 +2,78 @@ package cache import ( "encoding/json" - "gost/config" - "log" + testconfig "gost/tests/config" + "gost/util" "testing" "time" ) -type CacheTest struct { +type cacheTest struct { X int Y int Z int } func TestCache(t *testing.T) { - const cacheExpireTime = 1 * time.Second + const cacheExpireTime = time.Millisecond * 700 - var queries = []string{ - "test:x%2==0", - "test:(x+y)%z>1", - "test:z>550", - "thisNeedsToExpire", + var cacheKeys = []combinedKey{ + combinedKey{Key: "/testKey1", DataKey: "testDataKey1"}, + combinedKey{Key: "/testKey2", DataKey: "testDataKey2"}, + combinedKey{Key: "/testTheThirdKey", DataKey: "testTheThirdDataKey"}, + combinedKey{Key: "/thisNeedsToExpire", DataKey: "thisNeedsToDataExpire"}, } - var items []CacheTest + var items []cacheTest var cachedItems []*Cache var expiringItem *Cache - config.InitTestsDatabase() + testconfig.InitTestsDatabase() StartCachingSystem(cacheExpireTime) defer StopCachingSystem() items = testInitItems(t) - testFetchInexistentCache(t, queries[0]) - cachedItems, expiringItem = testAddingToCache(t, items, queries) + testFetchInexistentCache(t, cacheKeys[0]) + cachedItems, expiringItem = testAddingToCache(t, items, cacheKeys) testFetchingFromCache(t, cachedItems) testRemovingFromCache(t, cachedItems) - testFetchInexistentCache(t, queries[1]) + testFetchInexistentCache(t, cacheKeys[1]) testExpiringItem(t, expiringItem, cacheExpireTime) } func testExpiringItem(t *testing.T, expiringItem *Cache, cacheExpireTime time.Duration) { - log.Println("Testing the expired cache invalidation system") + t.Log("[info] Testing the expired cache invalidation system") - time.Sleep(2 * cacheExpireTime) + time.Sleep(cacheExpireTime * 2) - it := QueryByKey(expiringItem.Query) + _, err := Query(expiringItem.Key, expiringItem.DataKey) - if it != nil { + if err == nil || err != ErrKeyInvalidated { t.Fatal("The cache items did not properly expire") } } -func testFetchInexistentCache(t *testing.T, mockQuery string) { - log.Println("Testing the cache querying system with inexistent or invalid data") +func testFetchInexistentCache(t *testing.T, mockQuery combinedKey) { + t.Log("[info] Testing the cache querying system with inexistent or invalid data") // Will never be added - data := QueryByKey("keySFAFSAGKAGHAJSKfhaskfhaskf") + var inexistentKey, _ = util.GenerateUUID() + var inexistentDataKey, _ = util.GenerateUUID() + data, _ := Query(inexistentKey, inexistentDataKey) if data != nil { - t.Fatal("Unexpected output from cache") + t.Fatal("[error] Unexpected output from cache") } // Will be added later during the test - data = QueryByKey(mockQuery) + data, _ = Query(mockQuery.Key, mockQuery.DataKey) if data != nil { - t.Fatal("Unexpected output from cache") + t.Fatal("[error] Unexpected output from cache") } } func testFetchingFromCache(t *testing.T, cachedItems []*Cache) { - log.Println("Testing the cache querying system with valid data") + t.Log("[info] Testing the cache querying system with valid data") var q1 *Cache var q2 *Cache @@ -79,31 +81,31 @@ func testFetchingFromCache(t *testing.T, cachedItems []*Cache) { i := 0 for i < 2 { - q1 = QueryByKey(cachedItems[0].Query) - q2 = QueryByKey(cachedItems[1].Query) - q3 = QueryByKey(cachedItems[2].Query) + q1, _ = Query(cachedItems[0].Key, cachedItems[0].DataKey) + q2, _ = Query(cachedItems[1].Key, cachedItems[1].DataKey) + q3, _ = Query(cachedItems[2].Key, cachedItems[2].DataKey) if q1 == nil || q2 == nil || q3 == nil { - t.Fatal("Cache didn't properly return test items") + t.Fatal("[error] Cache didn't properly return test items") } i++ } - if q1.Query != cachedItems[0].Query || q2.Query != cachedItems[1].Query || q3.Query != cachedItems[2].Query { - t.Fatal("Wrong cache values were returned") + if q1.Key != cachedItems[0].Key || q2.Key != cachedItems[1].Key || q3.Key != cachedItems[2].Key { + t.Fatal("[error] Wrong cache values were returned") } } -func testAddingToCache(t *testing.T, items []CacheTest, queries []string) ([]*Cache, *Cache) { - log.Println("Testing the data caching system") +func testAddingToCache(t *testing.T, items []cacheTest, cacheKeys []combinedKey) ([]*Cache, *Cache) { + t.Log("[info] Testing the data caching system") - var cachedItems []*Cache + var cachedItems = make([]*Cache, 3) var expiringCacheItem *Cache - q1 := make([]CacheTest, 0) - q2 := make([]CacheTest, 0) - q3 := make([]CacheTest, 0) + var q1 []cacheTest + var q2 []cacheTest + var q3 []cacheTest // First type for i := 0; i < len(items); i++ { @@ -113,11 +115,11 @@ func testAddingToCache(t *testing.T, items []CacheTest, queries []string) ([]*Ca } j1, _ := json.MarshalIndent(q1, "", " ") c1 := &Cache{ - Query: queries[0], - Data: j1, + Key: cacheKeys[0].Key, + DataKey: cacheKeys[0].DataKey, + Data: j1, } - c1.Cache() - cachedItems = append(cachedItems, c1) + cachedItems[0] = c1 // Second type for i := 0; i < len(items); i++ { @@ -127,11 +129,11 @@ func testAddingToCache(t *testing.T, items []CacheTest, queries []string) ([]*Ca } j2, _ := json.MarshalIndent(q2, "", " ") c2 := &Cache{ - Query: queries[1], - Data: j2, + Key: cacheKeys[1].Key, + DataKey: cacheKeys[1].DataKey, + Data: j2, } - c2.Cache() - cachedItems = append(cachedItems, c2) + cachedItems[1] = c2 // Third type for i := 0; i < len(items); i++ { @@ -141,35 +143,42 @@ func testAddingToCache(t *testing.T, items []CacheTest, queries []string) ([]*Ca } j3, _ := json.MarshalIndent(q3, "", " ") c3 := &Cache{ - Query: queries[2], - Data: j3, + Key: cacheKeys[2].Key, + DataKey: cacheKeys[2].DataKey, + Data: j3, } - c3.Cache() - cachedItems = append(cachedItems, c3) + cachedItems[2] = c3 // Expiring type expiringCacheItem = &Cache{ - Query: queries[3], - Data: j1, + Key: cacheKeys[3].Key, + DataKey: cacheKeys[3].DataKey, + Data: j1, } + expiringCacheItem.Cache() + for _, cachedItem := range cachedItems { + cachedItem.Cache() + } + + time.Sleep(500 * time.Millisecond) return cachedItems, expiringCacheItem } func testRemovingFromCache(t *testing.T, cachedItems []*Cache) { - log.Println("Testing the cache invalidation system") + t.Log("[info] Testing the cache invalidation system") for _, it := range cachedItems { it.Invalidate() } } -func testInitItems(t *testing.T) []CacheTest { - var items []CacheTest +func testInitItems(t *testing.T) []cacheTest { + var items []cacheTest for i := 1; i < 1000; i++ { - items = append(items, CacheTest{ + items = append(items, cacheTest{ X: i, Y: i * 11 / 3, Z: i * 3, diff --git a/config/app_config.go b/config/app_config.go old mode 100644 new mode 100755 index 059402d..aeb2f25 --- a/config/app_config.go +++ b/config/app_config.go @@ -1,37 +1,36 @@ -// Package used for application configuration +// Package config is used for application configuration management package config import ( "encoding/json" "io/ioutil" "log" - "os" -) - -const ( - ENV_APPLICATION_NAME = "GOST_TESTAPP_NAME" - ENV_API_INSTANCE = "GOST_TESTAPP_INSTANCE" - ENV_HTTP_SERVER_ADDRESS = "GOST_TESTAPP_HTTP" ) // Application configuration file path var appConfigFile = "config/app.json" -// Application descriptive variables var ( - ApplicationName string - ApiInstance string - HttpServerAddress string + // ApplicationName represents the name of the application + ApplicationName string + + // APIInstance represents the current instance (version, server, signature etc) of the api + APIInstance string + + // HTTPServerAddress represents the address at which the HTTP server is started and listening + HTTPServerAddress string ) // Struct with the sole purpose of easier serialization // and deserialization of configuration data type appConfigHolder struct { ApplicationName string `json:"applicationName"` - ApiInstance string `json:"apiInstance"` - HttpServerAddress string `json:"httpServerAddress"` + APIInstance string `json:"apiInstance"` + HTTPServerAddress string `json:"httpServerAddress"` } +// InitApp initializes the application by reading the functional parameters +// from the configuration file func InitApp(appConfigPath string) { if len(appConfigPath) != 0 { appConfigFile = appConfigPath @@ -40,24 +39,16 @@ func InitApp(appConfigPath string) { configData := &appConfigHolder{} data, err := ioutil.ReadFile(appConfigFile) - if err != nil { log.Fatal(err) } err = json.Unmarshal(data, &configData) - if err != nil { log.Fatal(err) } ApplicationName = configData.ApplicationName - ApiInstance = configData.ApiInstance - HttpServerAddress = configData.HttpServerAddress -} - -func InitTestsApp() { - ApplicationName = os.Getenv(ENV_APPLICATION_NAME) - ApiInstance = os.Getenv(ENV_API_INSTANCE) - HttpServerAddress = os.Getenv(ENV_HTTP_SERVER_ADDRESS) + APIInstance = configData.APIInstance + HTTPServerAddress = configData.HTTPServerAddress } diff --git a/config/app_config_test.go b/config/app_config_test.go old mode 100644 new mode 100755 index 9bff50e..f3f75b4 --- a/config/app_config_test.go +++ b/config/app_config_test.go @@ -1,10 +1,8 @@ package config -import ( - "testing" -) +import "testing" -const appFilePath = "test_data/app.json" +const appFilePath = "../gost/config/app.json" func TestAppConfig(t *testing.T) { InitApp(appFilePath) @@ -13,11 +11,11 @@ func TestAppConfig(t *testing.T) { t.Fatal("Application name was not properly loaded from the config file!") } - if len(ApiInstance) == 0 { + if len(APIInstance) == 0 { t.Fatal("Api instance version was not properly loaded from the config file!") } - if len(HttpServerAddress) == 0 { + if len(HTTPServerAddress) == 0 { t.Fatal("Api http server address was not properly loaded from the config file!") } } diff --git a/config/db_config.go b/config/db_config.go old mode 100644 new mode 100755 index 3abd3b5..c38ba2f --- a/config/db_config.go +++ b/config/db_config.go @@ -5,18 +5,12 @@ import ( "encoding/json" "io/ioutil" "log" - "os" -) - -const ( - ENV_DB_NAME = "GOST_TESTAPP_DB_NAME" - ENV_DB_CONN = "GOST_TESTAPP_DB_CONN" ) // Database configuration file path -var dbConfigFileName string = "config/db.json" +var dbConfigFileName = "config/db.json" -// Struct for modelling the configuration json representing +// DbConfig is a struct for modelling the configuration json representing // The database connection details type DbConfig struct { DatabaseName string `json:"databaseName"` @@ -26,14 +20,26 @@ type DbConfig struct { Hosts []string `json:"hosts"` } -// The database connection string variable +// DbConnectionString represents the database connection string. // This variable needs to be initialized var DbConnectionString string -// The name of the database that will be used +// DbName represents the name of the database that will be used. // This variable needs to be initialized var DbName string +// InitDatabase initializes the production database +func InitDatabase(configFile string) { + if len(configFile) != 0 { + dbConfigFileName = configFile + } + + data := fetchAndDeserializeDbData(dbConfigFileName) + + DbName = data.DatabaseName + DbConnectionString = createConnectionString(data) +} + func fetchAndDeserializeDbData(filePath string) DbConfig { var configEntity DbConfig @@ -77,28 +83,3 @@ func createConnectionString(data DbConfig) string { return buf.String() } - -// Initialization of production database -func InitDatabase(configFile string) { - if len(configFile) != 0 { - dbConfigFileName = configFile - } - - data := fetchAndDeserializeDbData(dbConfigFileName) - - DbName = data.DatabaseName - DbConnectionString = createConnectionString(data) -} - -// Initialization of tests database -func InitTestsDatabase() { - dbName := os.Getenv(ENV_DB_NAME) - dbConn := os.Getenv(ENV_DB_CONN) - - if len(dbName) == 0 || len(dbConn) == 0 { - log.Fatal("Environment variables for the test database are not set!") - } - - DbName = dbName - DbConnectionString = dbConn -} diff --git a/config/db_config_test.go b/config/db_config_test.go old mode 100644 new mode 100755 index 7d5145e..7784571 --- a/config/db_config_test.go +++ b/config/db_config_test.go @@ -1,21 +1,19 @@ package config -import ( - "testing" -) +import "testing" -const dbFilePath = "test_data/db.json" +const dbFilePath = "../gost/config/db.json" func TestDbConfig(t *testing.T) { - InitDatabase(dbFilePath) + InitDatabase(dbFilePath) - testsDbCon := DbConnectionString - testsDbName := DbName + testsDbCon := DbConnectionString + testsDbName := DbName - switch { - case len(testsDbCon) == 0: - t.Fatal("Database connection string not loaded from config!") - case len(testsDbName) == 0: - t.Fatal("Database name could not be loaded from config!") - } + switch { + case len(testsDbCon) == 0: + t.Fatal("Database connection string not loaded from config!") + case len(testsDbName) == 0: + t.Fatal("Database name could not be loaded from config!") + } } diff --git a/config/route.go b/config/route.go old mode 100644 new mode 100755 index 84989b5..8eb8e90 --- a/config/route.go +++ b/config/route.go @@ -1,32 +1,33 @@ package config -// Http methods -const ( - GET_HTTP_METHOD = "GET" - POST_HTTP_METHOD = "POST" - PUT_HTTP_METHOD = "PUT" - DELETE_HTTP_METHOD = "DELETE" -) +// Action struct represents an action that an endpoint has +type Action struct { + Type string `json:"type"` + AllowAnonymous bool `json:"allowAnonymous"` + RequireAdmin bool `json:"requireAdmin"` +} // Route entity type Route struct { - Id string `json:"id"` - Pattern string `json:"pattern"` - Handlers map[string]string `json:"handlers"` + ID string `json:"id"` + Endpoint string `json:"endpoint"` + IsCacheable bool `json:"isCacheable"` + Actions map[string]*Action `json:"actions"` } -func (route *Route) Equal(otherRoute Route) bool { +// Equal determines if the current Route is equal to another Route +func (route *Route) Equal(otherRoute *Route) bool { switch { - case route.Id != otherRoute.Id: + case route.ID != otherRoute.ID: return false - case route.Pattern != otherRoute.Pattern: + case route.Endpoint != otherRoute.Endpoint: return false - case len(route.Handlers) != len(otherRoute.Handlers): + case len(route.Actions) != len(otherRoute.Actions): return false default: - for key, value := range route.Handlers { - if otherValue, found := otherRoute.Handlers[key]; found { - if value != otherValue { + for key, action := range route.Actions { + if otherAction, found := otherRoute.Actions[key]; found { + if !action.Equal(otherAction) { return false } } else { @@ -37,3 +38,17 @@ func (route *Route) Equal(otherRoute Route) bool { return true } } + +// Equal determines if the current Action is equal to another Action +func (action *Action) Equal(otherAction *Action) bool { + switch { + case action.Type != otherAction.Type: + return false + case action.AllowAnonymous != otherAction.AllowAnonymous: + return false + case action.RequireAdmin != otherAction.RequireAdmin: + return false + } + + return true +} diff --git a/config/routes_config.go b/config/routes_config.go old mode 100644 new mode 100755 index 0f2359e..360fec6 --- a/config/routes_config.go +++ b/config/routes_config.go @@ -11,13 +11,10 @@ import ( // Routes configuration file path var routesConfigFile = "config/routes.json" -// Variable for storing all the routes that the api will have +// Routes is a variable used for storing all the routes that the api will have var Routes []Route -func InitTestsRoutes(routesString string) { - deserializeRoutes([]byte(routesString)) -} - +// InitRoutes initializes the routes based on a configuration file func InitRoutes(routesConfigPath string) { if len(routesConfigPath) != 0 { routesConfigFile = routesConfigPath @@ -32,7 +29,7 @@ func InitRoutes(routesConfigPath string) { deserializeRoutes(routesString) } -// Save all the active routes (Routes slice) in json format +// SaveRoutesConfiguration saves all the active routes (Routes slice) in json format // into the configuration file func SaveRoutesConfiguration() error { if len(Routes) == 0 { @@ -53,12 +50,12 @@ func SaveRoutesConfiguration() error { return nil } -// Add a new route and make it active +// AddRoute adds a new route and makes it active func AddRoute(route *Route, saveChangesToConfigFile bool) error { initialLength := len(Routes) for _, r := range Routes { - if r.Id == route.Id { + if r.ID == route.ID { return errors.New("Route already exists!") } } @@ -70,13 +67,13 @@ func AddRoute(route *Route, saveChangesToConfigFile bool) error { return saveChanges(err, saveChangesToConfigFile, SaveRoutesConfiguration) } -// Disable and remove a certain route -func RemoveRoute(routeId string, saveChangesToConfigFile bool) error { +// RemoveRoute disables and removes a certain route +func RemoveRoute(routeID string, saveChangesToConfigFile bool) error { initialLength := len(Routes) index := -1 for ind, route := range Routes { - if route.Id == routeId { + if route.ID == routeID { index = ind break } @@ -94,10 +91,10 @@ func RemoveRoute(routeId string, saveChangesToConfigFile bool) error { return saveChanges(err, saveChangesToConfigFile, SaveRoutesConfiguration) } -// Modify the state and information of a certain route -func ModifyRoute(routeId string, newRouteData Route, saveChangesToConfigFile bool) error { +// ModifyRoute modifies the state and information of a certain route +func ModifyRoute(routeID string, newRouteData Route, saveChangesToConfigFile bool) error { for i := 0; i < len(Routes); i++ { - if Routes[i].Id == routeId { + if Routes[i].ID == routeID { Routes[i] = newRouteData return saveChanges(nil, saveChangesToConfigFile, SaveRoutesConfiguration) @@ -107,10 +104,10 @@ func ModifyRoute(routeId string, newRouteData Route, saveChangesToConfigFile boo return errors.New("Route was not found for modification!") } -// Get a Route entity from the active routes list, base on its ID -func GetRoute(routeId string) *Route { +// GetRoute fetches a Route entity from the active routes list, base on its ID +func GetRoute(endpoint string) *Route { for _, route := range Routes { - if route.Id == routeId { + if route.Endpoint == endpoint { return &route } } diff --git a/config/routes_config_test.go b/config/routes_config_test.go old mode 100644 new mode 100755 index c2e04ee..d3b9ea2 --- a/config/routes_config_test.go +++ b/config/routes_config_test.go @@ -4,86 +4,12 @@ import ( "testing" ) -const routesFilePath = "test_data/routes.json" +const routesFilePath = "../gost/config/routes.json" func TestRoutesConfig(t *testing.T) { - configRoutes(t) - - route := addRoute(t) - - r := modifyRoute(t, route.Id) - - removeRoute(t, r.Id) -} - -func configRoutes(t *testing.T) { InitRoutes(routesFilePath) if Routes == nil { t.Fatal("Retrieving the routes from the configuration file has failed!") } - - err := SaveRoutesConfiguration() - - if err != nil { - t.Fatal("Error while saving the routes to the configuration file!") - } -} - -func addRoute(t *testing.T) Route { - route := Route{ - Id: "TestRoute", - Pattern: "/test/pattern/{testVar}", - Handlers: map[string]string{ - GET_HTTP_METHOD: "Api.GetTest", - POST_HTTP_METHOD: "Api.PostTest", - PUT_HTTP_METHOD: "Api.PutTest", - DELETE_HTTP_METHOD: "Api.DeleteTest", - }, - } - - err := AddRoute(&route, true) - if err != nil { - t.Fatal("Adding new routes failed!") - } - - return route -} - -func modifyRoute(t *testing.T, routeId string) Route { - r := GetRoute(routeId) - if r == nil { - t.Fatal("Route fetching failed!") - } - - r.Id = "TestRouteModified" - r.Pattern = "/test/patternModified" - - err := ModifyRoute(routeId, *r, true) - if err != nil { - t.Fatal("Route modification failed!") - } - - r2 := GetRoute(r.Id) - if r2 == nil { - t.Fatal("Modified route fetching failed!") - } - - if !r2.Equal(*r) { - t.Fatal("Route modification did not properly work!") - } - - return *r2 -} - -func removeRoute(t *testing.T, routeId string) { - err := RemoveRoute(routeId, true) - if err != nil { - t.Fatal("Removal of routes failed!") - } - - route := GetRoute(routeId) - if route != nil { - t.Fatal("Route hasn't been successfully removed from the collection!") - } } diff --git a/config/test_data/app.json b/config/test_data/app.json deleted file mode 100644 index e503be2..0000000 --- a/config/test_data/app.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "applicationName": "gost", - "apiInstance": "/v1/", - "httpServerAddress": "0.0.0.0:8080", - "swaggerRedirectAddress": "127.0.0.1:8000" -} \ No newline at end of file diff --git a/config/test_data/db.json b/config/test_data/db.json deleted file mode 100644 index 3464e2f..0000000 --- a/config/test_data/db.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "databaseName": "gost_db", - "user": "", - "pass": "", - "driver": "mongodb", - "hosts": [ - "localhost", - "localhost:5555", - "localhost:7987" - ] -} \ No newline at end of file diff --git a/config/test_data/routes.json b/config/test_data/routes.json deleted file mode 100644 index dbb89f8..0000000 --- a/config/test_data/routes.json +++ /dev/null @@ -1,72 +0,0 @@ -[ - { - "id": "UsersRoute", - "pattern": "/users", - "handlers": { - "DELETE": "DeleteUser", - "GET": "GetUser", - "POST": "PostUser", - "PUT": "PutUser" - } - }, - { - "id": "ProductsRoute", - "pattern": "/products", - "handlers": { - "DELETE": "DeleteProduct", - "GET": "GetProduct", - "POST": "PostProduct", - "PUT": "PutProduct" - } - }, - { - "id": "ShopsRoute", - "pattern": "/shops", - "handlers": { - "DELETE": "DeleteShop", - "GET": "GetShop", - "POST": "PostShop", - "PUT": "PutShop" - } - }, - { - "id": "ProductCategoriesRoute", - "pattern": "/products/categories", - "handlers": { - "DELETE": "DeleteProductCategory", - "GET": "GetProductCategory", - "POST": "PostProductCategory", - "PUT": "PutProductCategory" - } - }, - { - "id": "FlagsRoute", - "pattern": "/flags", - "handlers": { - "DELETE": "DeleteFlag", - "GET": "GetFlag", - "POST": "PostFlag", - "PUT": "PutFlag" - } - }, - { - "id": "OrdersRoute", - "pattern": "/orders", - "handlers": { - "DELETE": "DeleteOrder", - "GET": "GetOrder", - "POST": "PostOrder", - "PUT": "PutOrder" - } - }, - { - "id": "UserSessionsRoute", - "pattern": "/users/sessions", - "handlers": { - "DELETE": "DeleteUserSession", - "GET": "GetUserSession", - "POST": "PostUserSession", - "PUT": "PutUserSession" - } - } -] \ No newline at end of file diff --git a/dbmodels/dbmodels.go b/dbmodels/dbmodels.go deleted file mode 100644 index 3facee9..0000000 --- a/dbmodels/dbmodels.go +++ /dev/null @@ -1,7 +0,0 @@ -package dbmodels - -// Interface for defining the models as Objects which -// Object if it can be compared with other objects -type Object interface { - Equal(obj Object) bool -} diff --git a/dbmodels/user.go b/dbmodels/user.go deleted file mode 100644 index a263b94..0000000 --- a/dbmodels/user.go +++ /dev/null @@ -1,77 +0,0 @@ -package dbmodels - -import ( - "gopkg.in/mgo.v2/bson" -) - -// Account type constants -const ( - CLIENT_ACCOUNT_TYPE = 0 - ADMINISTRATOR_ACCOUNT_TYPE = 1 -) - -// Struct representing an user account. This is a database dbmodels -type User struct { - Id bson.ObjectId `bson:"_id" json:"id"` - - Email string `bson:"email,omitempty" json:"email"` - Password string `bson:"password,omitempty" json:"password"` - AccountType int `bson:"accountType,omitempty" json:"accountType"` - Token string `bson:"token,omitempty" json:"token"` - - FirstName string `bson:"firstName,omitempty" json:"firstName"` - MiddleName string `bson:"middleName,omitempty" json:"middleName"` - LastName string `bson:"lastName,omitempty" json:"lastName"` - CompanyName string `bson:"companyName,omitempty" json:"companyName"` - Sex rune `bson:"sex,omitempty" json:"sex"` - Country string `bson:"country,omitempty" json:"country"` - State string `bson:"state,omitempty" json:"state"` - City string `bson:"city,omitempty" json:"city"` - Address string `bson:"address,omitempty" json:"address"` - PostalCode int `bson:"postalCode,omitempty" json:"postalCode"` - Picture string `bson:"picture,omitempty" json:"picture"` -} - -func (user *User) Equal(obj Object) bool { - otherUser, ok := obj.(*User) - if !ok { - return false - } - - switch { - case user.Id != otherUser.Id: - return false - case user.Token != otherUser.Token: - return false - case user.Password != otherUser.Password: - return false - case user.AccountType != otherUser.AccountType: - return false - case user.FirstName != otherUser.FirstName: - return false - case user.MiddleName != otherUser.MiddleName: - return false - case user.LastName != otherUser.LastName: - return false - case user.Email != otherUser.Email: - return false - case user.CompanyName != otherUser.CompanyName: - return false - case user.Sex != otherUser.Sex: - return false - case user.Country != otherUser.Country: - return false - case user.State != otherUser.State: - return false - case user.City != otherUser.City: - return false - case user.Address != otherUser.Address: - return false - case user.PostalCode != otherUser.PostalCode: - return false - case user.Picture != otherUser.Picture: - return false - } - - return true -} diff --git a/dbmodels/user_session.go b/dbmodels/user_session.go deleted file mode 100644 index ff8a3a7..0000000 --- a/dbmodels/user_session.go +++ /dev/null @@ -1,34 +0,0 @@ -package dbmodels - -import ( - "gopkg.in/mgo.v2/bson" - "time" -) - -type UserSession struct { - Id bson.ObjectId `bson:"_id" json:"id"` - - UserId bson.ObjectId `bson:"userId,omitempty" json:"userId"` - Token string `bson:"token,omitempty" json:"token"` - ExpireDate time.Time `bson:"expireDate,omitempty" json:"expireDate"` -} - -func (userSession *UserSession) Equal(obj Object) bool { - otherSession, ok := obj.(*UserSession) - if !ok { - return false - } - - switch { - case userSession.Id != otherSession.Id: - return false - case userSession.Token != otherSession.Token: - return false - case userSession.UserId != otherSession.UserId: - return false - case !userSession.ExpireDate.Truncate(time.Millisecond).Equal(otherSession.ExpireDate.Truncate(time.Millisecond)): - return false - } - - return true -} diff --git a/email/email.go b/email/email.go new file mode 100755 index 0000000..cf6036e --- /dev/null +++ b/email/email.go @@ -0,0 +1,91 @@ +package email + +import ( + "bytes" + "fmt" + "net/mail" + "net/smtp" +) + +const ( + authServer = "smtp.zoho.com" + smtpServer = "smtp.zoho.com:587" + gostUsername = "gostwebframework@zoho.com" + gostPassword = "gostwebframework" + senderName = "GostWebFramework" + senderEmail = "gostwebframework@zoho.com" +) + +var ( + sender = mail.Address{Name: senderName, Address: senderEmail} + authorization = smtp.PlainAuth("", gostUsername, gostPassword, authServer) +) + +var basicHeader = map[string]string{ + "From": sender.String(), + "MIME-Version": "1.0", + "Content-Type": "text/html; charset=\"utf-8\"", + "Content-Transfer-Encoding": "base64", +} + +// Email struct is used to send an email message +type Email struct { + recipient []string + body string + header map[string]string +} + +// NewEmail creates a new email message +func NewEmail() *Email { + return &Email{ + header: basicHeader, + } +} + +// Send sends the email message +func (email *Email) Send() error { + var content = createContent(email.header, email.body) + + return smtp.SendMail(smtpServer, + authorization, + sender.Address, + email.recipient, + content) +} + +// SetRecipient sets the receiver of the email +func (email *Email) SetRecipient(address string) { + var recipient = mail.Address{ + Address: address, + } + + email.recipient = []string{recipient.Address} + email.header["To"] = recipient.String() +} + +// SetSubject sets the subject of the email +func (email *Email) SetSubject(subject string) { + email.header["Subject"] = subject +} + +// SetBody sets the body of the email +func (email *Email) SetBody(body string) { + email.body = body +} + +func createContent(header map[string]string, body string) []byte { + var message bytes.Buffer + + // Header + for key, value := range header { + message.WriteString(fmt.Sprintf("%s: %s\r\n", key, value)) + } + + // Body delimiter + message.WriteString("\r\n") + + // Body + message.WriteString(body) + + return message.Bytes() +} diff --git a/email/template_funcs.go b/email/template_funcs.go new file mode 100755 index 0000000..8af6856 --- /dev/null +++ b/email/template_funcs.go @@ -0,0 +1,25 @@ +package email + +// SendAccountActivationEmail sends an account activation confirmation email to a user +func SendAccountActivationEmail(email, urlEndpoint string) error { + emailBody := ParseTemplate(activateAccountTemplate, urlEndpoint) + + mail := NewEmail() + mail.SetRecipient(email) + mail.SetSubject(activateAccountSubject) + mail.SetBody(emailBody) + + return mail.Send() +} + +// SendPasswordResetEmail sends an account password reset instructions email to a user +func SendPasswordResetEmail(email, urlEndpoint string) error { + emailBody := ParseTemplate(resetPasswordTemplate, urlEndpoint) + + mail := NewEmail() + mail.SetRecipient(email) + mail.SetSubject(resetPasswordSubject) + mail.SetBody(emailBody) + + return mail.Send() +} diff --git a/email/templates.go b/email/templates.go new file mode 100755 index 0000000..ec604db --- /dev/null +++ b/email/templates.go @@ -0,0 +1,57 @@ +package email + +import "fmt" + +// ParseTemplate parses a standard HTML template and places the parameters in the +// template's indicated parts +func ParseTemplate(templateFormat string, params ...interface{}) string { + return fmt.Sprintf(templateFormat, params) +} + +const ( + activateAccountSubject = `Welcome on board` + activateAccountTemplate = ` + + + + + +

Hello there,

+

Welcome to the GOST web framework!

+

+

To activate your account, please use the following link: %s

+

+

Cheers!

+

GOST Team

+ +` +) + +const ( + resetPasswordSubject = `Password reset` + resetPasswordTemplate = ` + + + + + +

Hello there,


+

A password reset was requested for this account.

+ If it was not you who made the request, please disregard this email +

+

+

To reset your password, please use the following link: %s

+

+

Cheers!

+

GOST Team

+ +` +) diff --git a/filter/api_data_extractors.go b/filter/api_data_extractors.go new file mode 100755 index 0000000..e0d3f31 --- /dev/null +++ b/filter/api_data_extractors.go @@ -0,0 +1,46 @@ +package filter + +import ( + "errors" + "net/url" + "strconv" + "strings" + + "gopkg.in/mgo.v2/bson" +) + +// GetIntParameter extracts an integer value from url paramters, based on its name +func GetIntParameter(paramName string, reqForm url.Values) (int, bool, error) { + value := reqForm.Get(paramName) + if value == "" { + return -1, false, nil + } + + if intVal, err := strconv.Atoi(value); err == nil { + return intVal, true, nil + } + + errMsg := []string{"The", paramName, "parameter is not in the correct format"} + return -1, true, errors.New(strings.Join(errMsg, " ")) +} + +// GetStringParameter extracts a string value from url paramters, based on its name +func GetStringParameter(paramName string, reqForm url.Values) (string, bool) { + value := reqForm.Get(paramName) + + return value, value != "" +} + +// GetIDParameter extracts a bson.ObjectID value from url paramters, based on its name +func GetIDParameter(paramName string, reqForm url.Values) (bson.ObjectId, bool, error) { + id := reqForm.Get(paramName) + if id == "" { + return "", false, nil + } + + if !bson.IsObjectIdHex(id) { + return "", true, errors.New("The id parameter is not a valid bson.ObjectId") + } + + return bson.ObjectIdHex(id), true, nil +} diff --git a/filter/apifilter/api_data_extractors.go b/filter/apifilter/api_data_extractors.go deleted file mode 100644 index 6a32279..0000000 --- a/filter/apifilter/api_data_extractors.go +++ /dev/null @@ -1,43 +0,0 @@ -package apifilter - -import ( - "errors" - "gopkg.in/mgo.v2/bson" - "net/url" - "strconv" - "strings" -) - -func GetIntValueFromParams(paramName string, reqForm url.Values) (int, error, bool) { - value := reqForm.Get(paramName) - if value == "" { - errMsg := []string{"The", paramName, "parameter was not specified"} - return -1, errors.New(strings.Join(errMsg, " ")), false - } - - if intVal, err := strconv.Atoi(value); err == nil { - return intVal, nil, true - } - - errMsg := []string{"The", paramName, "parameter is not in the correct format"} - return -1, errors.New(strings.Join(errMsg, " ")), true -} - -func GetStringValueFromParams(paramName string, reqForm url.Values) (string, bool) { - value := reqForm.Get(paramName) - - return value, value != "" -} - -func GetIdFromParams(reqForm url.Values) (bson.ObjectId, error, bool) { - id := reqForm.Get("id") - if id == "" { - return "", errors.New("The id parameter was not specified"), false - } - - if !bson.IsObjectIdHex(id) { - return "", errors.New("The id parameter is not a valid bson.ObjectId"), true - } - - return bson.ObjectIdHex(id), nil, true -} diff --git a/filter/apifilter/transactions_api_filter.go b/filter/apifilter/transactions_api_filter.go old mode 100644 new mode 100755 index 1c881d5..a5dfef3 --- a/filter/apifilter/transactions_api_filter.go +++ b/filter/apifilter/transactions_api_filter.go @@ -1,14 +1,15 @@ package apifilter import ( - "gost/models" + "gost/orm/models" ) +// CheckTransactionIntegrity checks if a Transaction has all the compulsory fields populated func CheckTransactionIntegrity(transaction *models.Transaction) bool { switch { - case len(transaction.Payer.Id) == 0: + case len(transaction.Payer.ID) == 0: return false - case len(transaction.Receiver.Id) == 0: + case len(transaction.Receiver.ID) == 0: return false case transaction.Type < 0: return false diff --git a/filter/apifilter/user_sessions_api_filter.go b/filter/apifilter/user_sessions_api_filter.go deleted file mode 100644 index a3d000d..0000000 --- a/filter/apifilter/user_sessions_api_filter.go +++ /dev/null @@ -1,16 +0,0 @@ -package apifilter - -import ( - "gost/models" -) - -func CheckUserSessionIntegrity(userSession *models.UserSession) bool { - switch { - case len(userSession.User.Id) == 0: - return false - case len(userSession.Token) == 0: - return false - } - - return true -} diff --git a/filter/apifilter/users_api_filter.go b/filter/apifilter/users_api_filter.go deleted file mode 100644 index 54cfd40..0000000 --- a/filter/apifilter/users_api_filter.go +++ /dev/null @@ -1,16 +0,0 @@ -package apifilter - -import ( - "gost/models" -) - -func CheckUserIntegrity(user *models.User) bool { - switch { - case len(user.Email) == 0: - return false - case len(user.Token) == 0: - return false - } - - return true -} diff --git a/filter/http_requests_filter.go b/filter/http_requests_filter.go old mode 100644 new mode 100755 index 5395935..10de325 --- a/filter/http_requests_filter.go +++ b/filter/http_requests_filter.go @@ -1,39 +1,46 @@ package filter import ( - "errors" - "net/http" + "errors" + "net/http" ) var ( - NoContentError = errors.New("No content has been received") - InvalidFormFormatError = errors.New("The request form has an invalid format") + // ErrNoContent shows that the underlying HTTP request doesn't contain any data/content + ErrNoContent = errors.New("No content has been received") + + // ErrInvalidFormFormat shows that the underlying HTTP request has its data or form in a incorrect or unparsable format + ErrInvalidFormFormat = errors.New("The request form has an invalid format") ) -func CheckMethodAndParseContent(r *http.Request) (error, int) { - if r.ContentLength == 0 { - if r.Method == "POST" || r.Method == "PUT" { - return NoContentError, http.StatusBadRequest - } - } +// CheckMethodAndParseContent performs validity checks on a request based on the HTTP method used. +// Checks are made for data content if the methods are POST or PUT, and if the url form can be correctly parsed +func ParseRequestContent(request *http.Request) (int, error) { + if request.ContentLength == 0 { + if request.Method == "POST" || request.Method == "PUT" { + return http.StatusBadRequest, ErrNoContent + } + } - err := r.ParseForm() + err := request.ParseForm() - if err != nil { - return InvalidFormFormatError, http.StatusBadRequest - } + if err != nil { + return http.StatusBadRequest, ErrInvalidFormFormat + } - return nil, -1 + return -1, nil } +// CheckNotNull verifies if the given []byte data is nil or represents the word "null". +// In the above stated cases, the method returns false func CheckNotNull(data []byte) bool { - if data == nil { - return false - } + if data == nil { + return false + } - if string(data) == "null" { - return false - } + if string(data) == "null" { + return false + } - return true + return true } diff --git a/gost/config/app.json b/gost/config/app.json old mode 100644 new mode 100755 index 4640ce3..e9812b4 --- a/gost/config/app.json +++ b/gost/config/app.json @@ -1,5 +1,5 @@ { "applicationName": "gost", "apiInstance": "/gost/", - "httpServerAddress": "0.0.0.0:8080" + "httpServerAddress": "0.0.0.0:1234" } \ No newline at end of file diff --git a/gost/config/db.json b/gost/config/db.json old mode 100644 new mode 100755 diff --git a/gost/config/devroutes.json b/gost/config/devroutes.json new file mode 100755 index 0000000..75112e0 --- /dev/null +++ b/gost/config/devroutes.json @@ -0,0 +1,14 @@ +{ + "id": "DevApiRoute", + "endpoint": "/dev", + "actions": { + "CreateAppUser": { + "type": "POST", + "allowAnonymous": true + }, + "ActivateAppUser": { + "type": "GET", + "allowAnonymous": true + } + } +} \ No newline at end of file diff --git a/gost/config/routes.json b/gost/config/routes.json old mode 100644 new mode 100755 index 5aee31f..05d737f --- a/gost/config/routes.json +++ b/gost/config/routes.json @@ -1,31 +1,69 @@ [ { - "id": "UsersRoute", - "pattern": "/users", - "handlers": { - "DELETE": "DeleteUser", - "GET": "GetUser", - "POST": "PostUser", - "PUT": "PutUser" + "id": "TransactionsRoute", + "endpoint": "/transactions", + "actions": { + "CreateTransaction": { + "type": "POST", + "allowAnonymous": false, + "requireAdmin": false + }, + "GetTransaction": { + "type": "GET", + "allowAnonymous": false, + "requireAdmin": false + } } }, { - "id": "UserSessionsRoute", - "pattern": "/users/login", - "handlers": { - "DELETE": "DeleteUserSession", - "GET": "GetUserSession", - "POST": "PostUserSession", - "PUT": "PutUserSession" + "id": "AuthorizationRoute", + "endpoint": "/auth", + "actions": { + "CreateSession": { + "type": "POST", + "allowAnonymous": true + }, + "GetAllSessions": { + "type": "GET", + "allowAnonymous": false, + "requireAdmin": false + }, + "KillSession": { + "type": "POST", + "allowAnonymous": false, + "requireAdmin": false + }, + "ActivateAccount": { + "type": "POST", + "allowAnonymous": true + }, + "ResendAccountActivationEmail": { + "type": "GET", + "allowAnonymous": true + }, + "RequestResetPassword": { + "type": "POST", + "allowAnonymous": true + }, + "ResetPassword": { + "type": "POST", + "allowAnonymous": true + } } }, { - "id": "TransactionsRoute", - "pattern": "/transactions", - "handlers": { - "GET": "GetTransaction", - "POST": "PostTransaction", - "DELETE": "DeleteTransaction" + "id": "ValuesRoute", + "endpoint": "/values", + "actions": { + "Get": { + "type": "GET", + "allowAnonymous": false, + "requireAdmin": false + }, + "GetAnonymous": { + "type": "GET", + "allowAnonymous": true + } } } ] \ No newline at end of file diff --git a/gost/main.go b/gost/main.go old mode 100644 new mode 100755 index d43e1d1..2f94156 --- a/gost/main.go +++ b/gost/main.go @@ -1,10 +1,15 @@ package main import ( - "gost/api/userapi" + "gost/api/app/transactionapi" + "gost/api/framework/authapi" + "gost/api/framework/devapi" + "gost/api/framework/valuesapi" + "gost/auth/cookies" "gost/cache" "gost/config" "gost/httphandle" + "gost/security" "gost/servers" "gost/service" "log" @@ -15,14 +20,49 @@ import ( var numberOfProcessors = runtime.NumCPU() -// Add all the existing endpoints as part of this container -type ApiContainer struct { - userapi.UsersApi +// FrameworkAPIContainer is a struct used for boxing the framework's api endpoints. +// Add here all the framework endpoints that should be used by your application +type FrameworkAPIContainer struct { + authapi.AuthAPI + valuesapi.ValuesAPI +} + +// ApplicationAPIContainer is a struct used for boxing all the application's api endpoints. +// This also registers all the framework's vital endpoints +type ApplicationAPIContainer struct { + FrameworkAPIContainer + transactionapi.TransactionsAPI +} + +// DevAPIContainer is used only for development purposes. +// Register all the necessary api endpoints in the APIContainer type as this one just inherits it +type DevAPIContainer struct { + ApplicationAPIContainer + devapi.DevAPI +} + +// Application entry point - sets the behavior for the app +func main() { + startWebFramework() +} + +// startWebFramework performs the startup operations for the entire web framework +// and starts the actual http or https server used for listening for requests. +func startWebFramework() { + // Start listener for performing a graceful shutdown of the server + go listenForInterruptSignal() + + // Start a http or and https server depending on the program arguments + if len(os.Args) <= 1 || os.Args[1] == "http" { + servers.StartHTTPServer() + } else if os.Args[1] == "https" { + servers.StartHTTPSServer() + } } // Function for performing automatic initializations at application startup func init() { - var emptyConfigParam string = "" + var emptyConfigParam string // Initialize application configuration config.InitApp(emptyConfigParam) @@ -33,25 +73,21 @@ func init() { service.InitDbService() // Register the API endpoints - httphandle.SetApiInterface(new(ApiContainer)) -} + // httphandle.RegisterEndpoints(new(ApplicationAPIContainer)) ----- Use this API container when deploying in PRODUCTION + httphandle.RegisterEndpoints(new(DevAPIContainer)) //----- Use this API container when in development + devapi.InitDevRoutes() //----- Uncomment this line when in development -// Application entry point - sets the behavior for the app -func main() { - // Start listener for performing a graceful shutdown of the server - go listenForInterruptSignal() + // Start the caching system + cache.StartCachingSystem(cache.DefaultCacheExpireTime) - runtime.GOMAXPROCS(numberOfProcessors) + // Initialize the cookie store in the auth module + cookies.InitCookieStore() - // Start the caching system - cache.StartCachingSystem(cache.CACHE_EXPIRE_TIME) + // Initialize the encryption module + security.InitCipherModule() - // Start a http or and https server depending on the program arguments - if len(os.Args) <= 1 || os.Args[1] == "http" { - servers.StartHTTPServer() - } else if os.Args[1] == "https" { - servers.StartHTTPSServer() - } + // Set the app to use all the available processors + runtime.GOMAXPROCS(numberOfProcessors) } func listenForInterruptSignal() { @@ -60,6 +96,7 @@ func listenForInterruptSignal() { <-signalChan + log.Println("") log.Println("The server will now shut down gracefully...") service.CloseDbService() diff --git a/httphandle/api_call.go b/httphandle/api_call.go deleted file mode 100644 index 2f16826..0000000 --- a/httphandle/api_call.go +++ /dev/null @@ -1,137 +0,0 @@ -package httphandle - -import ( - "gost/api" - "gost/cache" - "gost/config" - "gost/filter" - "io" - "io/ioutil" - "log" - "net/http" - "reflect" -) - -var apiInterface interface{} - -func SetApiInterface(interf interface{}) { - apiInterface = interf -} - -func PerformApiCall(handlerName string, rw http.ResponseWriter, req *http.Request, route *config.Route) { - // Prepare data vector for an api/endpoint call - inputs := make([]reflect.Value, 1) - - // Create the variables containing request data - vars := createApiVars(req, rw, route) - if vars == nil { - return - } - - // Try giving the response directly from the cache if available - if cache.Status == cache.STATUS_ON { - if cachedData := cache.QueryByRequest(vars.RequestForm, route.Pattern); cachedData != nil { - if req.Method == api.GET { - GiveApiResponse(cachedData.StatusCode, cachedData.Data, rw, req, route.Pattern, cachedData.ContentType, cachedData.File) - return - } else { // Invalidate the cache if a modification, deletion or addition was made to this endpoint - go cachedData.Invalidate() - } - } - } - - // Populate the data vector for the api call - inputs[0] = reflect.ValueOf(vars) - - // Perform the call on the corresponding endpoint and function - // This is done by using reflection techniques - var respObjects []reflect.Value - apiMethod := reflect.ValueOf(apiInterface).MethodByName(route.Handlers[req.Method]) - - // Check for zero value - if apiMethod != *new(reflect.Value) { - respObjects = apiMethod.Call(inputs) - } else { - GiveApiStatus(http.StatusInternalServerError, rw, req, route.Pattern) - - log.Println("The endpoint method is either inexistent or incorrectly mapped. Please check the server configuration files!") - - return - } - - if respObjects == nil { - GiveApiStatus(http.StatusInternalServerError, rw, req, route.Pattern) - return - } - - // Extract the response from the endpoint into a concrete type - resp := respObjects[0].Interface().(api.ApiResponse) - - // Give the response to the api client - respond(vars, &resp, rw, req, route.Pattern) -} - -func respond(vars *api.ApiVar, resp *api.ApiResponse, rw http.ResponseWriter, req *http.Request, endpoint string) { - if resp.StatusCode == 0 { - resp.StatusCode = http.StatusInternalServerError - GiveApiMessage(resp.StatusCode, http.StatusText(resp.StatusCode), rw, req, endpoint) - } else if len(resp.ErrorMessage) > 0 { - GiveApiMessage(resp.StatusCode, resp.ErrorMessage, rw, req, endpoint) - } else { - if len(resp.ContentType) == 0 { - resp.ContentType = CONTENT_JSON - } - - GiveApiResponse(resp.StatusCode, resp.Message, rw, req, endpoint, resp.ContentType, resp.File) - - // Try caching the data only if a GET request was made - go func(vars *api.ApiVar, resp *api.ApiResponse, req *http.Request, endpoint string) { - if req.Method == api.GET && cache.Status == cache.STATUS_ON { - cacheResponse(vars, resp, endpoint) - } - }(vars, resp, req, endpoint) - } -} - -func cacheResponse(vars *api.ApiVar, resp *api.ApiResponse, endpoint string) { - if !(resp.StatusCode >= 200 && resp.StatusCode < 300) || len(resp.Message) == 0 { - return - } - - cacheEntity := &cache.Cache{ - Query: cache.MapKey(vars.RequestForm, endpoint), - Data: resp.Message, - StatusCode: resp.StatusCode, - ContentType: resp.ContentType, - File: resp.File, - } - - cacheEntity.Cache() -} - -func createApiVars(req *http.Request, rw http.ResponseWriter, route *config.Route) *api.ApiVar { - err, statusCode := filter.CheckMethodAndParseContent(req) - if err != nil { - GiveApiMessage(statusCode, err.Error(), rw, req, route.Pattern) - return nil - } - - body, err := convertBodyToReadableFormat(req.Body) - if err != nil { - GiveApiMessage(http.StatusBadRequest, err.Error(), rw, req, route.Pattern) - return nil - } - - vars := &api.ApiVar{ - RequestHeader: req.Header, - RequestForm: req.Form, - RequestContentLength: req.ContentLength, - RequestBody: body, - } - - return vars -} - -func convertBodyToReadableFormat(data io.ReadCloser) ([]byte, error) { - return ioutil.ReadAll(data) -} diff --git a/httphandle/api_handle.go b/httphandle/api_handle.go deleted file mode 100644 index 1c78167..0000000 --- a/httphandle/api_handle.go +++ /dev/null @@ -1,54 +0,0 @@ -package httphandle - -import ( - "gost/config" - "net/http" - "net/url" -) - -func ApiHandler(rw http.ResponseWriter, req *http.Request) { - path, err := parseRequestURL(req.URL) - - if err != nil { - GiveApiMessage(http.StatusBadRequest, "The format of the request URL is invalid", rw, req, path) - return - } - - route := findRoute(path) - - if route == nil { - GiveApiMessage(http.StatusNotFound, "404 - The requested page cannot be found", rw, req, path) - return - } - - handler := findApiMethod(req.Method, route) - - if handler == "" { - GiveApiMessage(http.StatusBadRequest, "The requested method is either not implemented, or not allowed", rw, req, path) - return - } - - PerformApiCall(handler, rw, req, route) -} - -func findRoute(pattern string) *config.Route { - for _, route := range config.Routes { - if route.Pattern == pattern { - return &route - } - } - - return nil -} - -func findApiMethod(requestMethod string, route *config.Route) string { - if handler, found := route.Handlers[requestMethod]; found { - return handler - } - - return "" -} - -func parseRequestURL(u *url.URL) (string, error) { - return u.Path[len(config.ApiInstance)-1:], nil -} diff --git a/httphandle/api_responses.go b/httphandle/api_responses.go deleted file mode 100644 index ac3b1bc..0000000 --- a/httphandle/api_responses.go +++ /dev/null @@ -1,80 +0,0 @@ -package httphandle - -import ( - "gost/filter" - "log" - "net/http" -) - -const ( - CONTENT_PLAIN_TEXT = "text/plain" - CONTENT_JSON = "application/json" -) - -func logRequest(statusCode int, message []byte, method, pattern string) { - if statusCode >= 400 { - log.Println(method, pattern, statusCode, string(message)) - } else { - log.Println(method, pattern, statusCode) - } -} - -func serveRawData(statusCode int, message []byte, rw http.ResponseWriter) { - if filter.CheckNotNull(message) { - rw.WriteHeader(statusCode) - rw.Write(message) - } else { - rw.WriteHeader(http.StatusNoContent) - } - -} - -func serveFile(rw http.ResponseWriter, req *http.Request, file string) { - // CORS headers - rw.Header().Set("Access-Control-Allow-Headers", "Content-Type, api_key, Authorization") - rw.Header().Set("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT") - rw.Header().Set("Access-Control-Allow-Origin", "*") - - http.ServeFile(rw, req, file) -} - -func GiveApiResponse(statusCode int, message []byte, rw http.ResponseWriter, req *http.Request, pattern, contentType, file string) { - // Handle redirect - if statusCode == http.StatusTemporaryRedirect { - http.Redirect(rw, req, string(message), statusCode) - } else { - - // Prepend necessary headers if existent or needed - if len(rw.Header().Get("Content-Type")) == 0 && len(contentType) > 0 { - rw.Header().Set("Content-Type", contentType) - } - - // Handle response type - if len(file) > 0 { - serveFile(rw, req, file) - } else { - serveRawData(statusCode, message, rw) - } - } - - // Log event - logRequest(statusCode, message, req.Method, pattern) -} - -func GiveApiMessage(statusCode int, message string, rw http.ResponseWriter, req *http.Request, pattern string) { - msg := []byte(message) - - GiveApiResponse(statusCode, msg, rw, req, pattern, CONTENT_PLAIN_TEXT, "") -} - -func GiveApiStatus(statusCode int, rw http.ResponseWriter, req *http.Request, pattern string) string { - msg := http.StatusText(statusCode) - - if len(msg) == 0 { - msg = StatusText(statusCode) - } - - GiveApiMessage(statusCode, msg, rw, req, pattern) - - return msg -} diff --git a/httphandle/custom_statuses.go b/httphandle/custom_statuses.go deleted file mode 100644 index fab24e8..0000000 --- a/httphandle/custom_statuses.go +++ /dev/null @@ -1,13 +0,0 @@ -package httphandle - -const ( - StatusTooManyRequests = 429 -) - -var statusText = map[int]string{ - StatusTooManyRequests: "Too many requests", -} - -func StatusText(statusCode int) string { - return statusText[statusCode] -} diff --git a/httphandle/request_handling.go b/httphandle/request_handling.go new file mode 100755 index 0000000..e12c692 --- /dev/null +++ b/httphandle/request_handling.go @@ -0,0 +1,126 @@ +package httphandle + +import ( + "errors" + "gost/api" + "gost/auth" + "gost/auth/identity" + "gost/config" + "net/http" + "net/url" + "strings" +) + +// Authorization encompasses the identity provided by the auth package +type Authorization struct { + Identity *identity.Identity + Error error +} + +// RequestHandler receives, parses and validates a HTTP request, which is then routed to the corresponding endpoint and method +func RequestHandler(rw http.ResponseWriter, req *http.Request) { + authChan := make(chan *Authorization) + go parseAuthorizationData(req, authChan) + + endpoint, actionName, isParseSuccessful := parseRequestURL(req.URL) + + if !isParseSuccessful { + close(authChan) + sendMessageResponse(http.StatusBadRequest, "The format of the request URL is invalid", rw, req, endpoint, actionName) + return + } + + route := config.GetRoute(endpoint) + + if route == nil { + close(authChan) + sendMessageResponse(http.StatusNotFound, "404 - The requested page cannot be found", rw, req, endpoint, actionName) + return + } + + if !validateEndpoint(req.Method, actionName, route) { + close(authChan) + sendMessageResponse(http.StatusUnauthorized, "The requested endpoint is either not implemented, or not allowed", rw, req, endpoint, actionName) + return + } + + userIdentity, authError := authorize(authChan, route.Actions[actionName]) + if authError != nil { + sendMessageResponse(http.StatusUnauthorized, authError.Error(), rw, req, route.Endpoint, actionName) + return + } + + RouteRequest(rw, req, route, actionName, userIdentity) +} + +func authorize(authChan chan *Authorization, routeAction *config.Action) (*identity.Identity, error) { + defer close(authChan) + + authorization := <-authChan + if authorization.Error != nil { + return nil, authorization.Error + } + + user := authorization.Identity + if (!routeAction.AllowAnonymous && user.IsAnonymous()) || (routeAction.RequireAdmin && !user.IsAdmin()) { + return nil, errors.New(api.StatusText(http.StatusUnauthorized)) + } + + return user, nil +} + +func parseAuthorizationData(req *http.Request, authChan chan *Authorization) { + // Recover in case the authorization channel was closed before the writing is done + defer func() { + recover() + }() + + identity, err := auth.Authorize(req.Header) + + authChan <- &Authorization{ + Identity: identity, + Error: err, + } +} + +func validateEndpoint(method, actionName string, route *config.Route) bool { + if action, found := route.Actions[actionName]; found { + return method == action.Type + } + + return false +} + +func parseRequestURL(u *url.URL) (string, string, bool) { + successfulParse := true + + defer func() { + if r := recover(); r != nil { + successfulParse = false + } + }() + + fullPath := u.Path[len(config.APIInstance)-1:] + + // Cut off any URL parameters and the last '/' character if present + if paramCharIndex := strings.Index(fullPath, "?"); paramCharIndex != -1 { + fullPath = fullPath[:paramCharIndex] + } + + if fullPath[len(fullPath)-1] == '/' { + fullPath = fullPath[:len(fullPath)-1] + } + + lastSeparatorIndex := strings.LastIndex(fullPath, "/") + if lastSeparatorIndex == -1 { + return "", "", false + } + + // Get the endpoint name + endpoint := fullPath[:lastSeparatorIndex] + + // Get the endpoint's action name + action := fullPath[lastSeparatorIndex+1:] + + return endpoint, action, successfulParse +} diff --git a/httphandle/request_responses.go b/httphandle/request_responses.go new file mode 100755 index 0000000..414ecdb --- /dev/null +++ b/httphandle/request_responses.go @@ -0,0 +1,77 @@ +package httphandle + +import ( + "bytes" + "gost/api" + "gost/filter" + "log" + "net/http" +) + +func sendResponse(statusCode int, message []byte, rw http.ResponseWriter, req *http.Request, endpoint, endpointAction, contentType, filePath string) { + // Handle redirect + if statusCode == http.StatusTemporaryRedirect { + http.Redirect(rw, req, string(message), statusCode) + } else { + + // Prepend necessary headers if existent or needed + if len(rw.Header().Get("Content-Type")) == 0 && len(contentType) > 0 { + rw.Header().Set("Content-Type", contentType) + } + + // Handle response type + if len(filePath) > 0 { + serveFile(rw, req, filePath) + } else { + serveRawData(statusCode, message, rw) + } + } + + // Log event + go logRequest(statusCode, message, req.Method, endpoint, endpointAction) +} + +func sendMessageResponse(statusCode int, message string, rw http.ResponseWriter, req *http.Request, endpoint, endpointAction string) { + msg := []byte(message) + + sendResponse(statusCode, msg, rw, req, endpoint, endpointAction, api.ContentTextPlain, "") +} + +func sendStatusResponse(statusCode int, rw http.ResponseWriter, req *http.Request, endpoint, endpointAction string) string { + message := api.StatusText(statusCode) + + sendMessageResponse(statusCode, message, rw, req, endpoint, endpointAction) + + return message +} + +func logRequest(statusCode int, message []byte, httpMethod, endpoint, endpointAction string) { + var requestPath bytes.Buffer + requestPath.WriteString(endpoint) + requestPath.WriteRune('/') + requestPath.WriteString(endpointAction) + + if statusCode >= 400 { + log.Println(httpMethod, requestPath.String(), statusCode, string(message)) + } else { + log.Println(httpMethod, requestPath.String(), statusCode) + } +} + +func serveRawData(statusCode int, message []byte, rw http.ResponseWriter) { + if filter.CheckNotNull(message) { + rw.WriteHeader(statusCode) + rw.Write(message) + } else { + rw.WriteHeader(http.StatusNoContent) + } +} + +func serveFile(rw http.ResponseWriter, req *http.Request, file string) { + // CORS headers + rw.Header().Set("Access-Control-Allow-Headers", "Content-Type, api_key, Authorization") + rw.Header().Set("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT") + rw.Header().Set("Access-Control-Allow-Origin", "*") + + http.ServeFile(rw, req, file) +} diff --git a/httphandle/request_routing.go b/httphandle/request_routing.go new file mode 100755 index 0000000..3f6e971 --- /dev/null +++ b/httphandle/request_routing.go @@ -0,0 +1,162 @@ +package httphandle + +import ( + "fmt" + "gost/api" + "gost/auth/identity" + "gost/cache" + "gost/config" + "gost/filter" + "io/ioutil" + "net/http" + "reflect" +) + +var endpointsContainer interface{} + +var zeroEndpointMethod = *new(reflect.Value) + +// RegisterEndpoints registers all the endpoints that are going to be mapped in the application +func RegisterEndpoints(container interface{}) { + endpointsContainer = container +} + +// RouteRequest parses the data from a HTTP request, determines which mapped endpoind needs to be called +// and forwards the request data to the found endpoint if it is valid. +func RouteRequest(rw http.ResponseWriter, req *http.Request, route *config.Route, endpointAction string, userIdentity *identity.Identity) { + // Prepare recover mechanism in case of panic + defer recoverFromError(rw, req, route.Endpoint, endpointAction) + + // Prepare data vector for an api/endpoint call + actionParameters := make([]reflect.Value, 1) + + // Create the variables containing request data + request := generateRequest(req, rw, route, endpointAction, userIdentity) + if request == nil { + return + } + + // Try giving the response directly from the cache if available or invalidate it if necessary + if respondFromCache(rw, req, route, endpointAction) { + return + } + + // Populate the data vector for the api call + actionParameters[0] = reflect.ValueOf(request) + + // Find out the name of the method where the request will be forwarded, + // based on the registered endpoints + endpointMethod := reflect.ValueOf(endpointsContainer).MethodByName(endpointAction) + + // Check if the searched action from the endpoint exists + if endpointMethod == zeroEndpointMethod { + message := "The endpoint action is either inexistent or incorrectly mapped. Please check the server configuration" + sendMessageResponse(http.StatusInternalServerError, message, rw, req, route.Endpoint, endpointAction) + return + } + + // Call the mapped method from the corresponding endpoint, using the extracted and parsed data from the HTTP request + respObjects := endpointMethod.Call(actionParameters) + if respObjects == nil { + sendStatusResponse(http.StatusInternalServerError, rw, req, route.Endpoint, endpointAction) + return + } + + // Extract the response from the endpoint into a concrete type + resp := respObjects[0].Interface().(api.Response) + + // Give the response to the api client + respond(&resp, rw, req, route.Endpoint, endpointAction) +} + +func recoverFromError(rw http.ResponseWriter, req *http.Request, pattern, endpointAction string) { + if err := recover(); err != nil { + message := fmt.Sprintf("%s", err) + sendMessageResponse(http.StatusInternalServerError, message, rw, req, pattern, endpointAction) + } +} + +func respondFromCache(rw http.ResponseWriter, req *http.Request, route *config.Route, endpointAction string) bool { + if cache.Status == cache.StatusOFF { + return false + } + + if !route.IsCacheable { + return false + } + + if cachedData, err := cache.Query(route.Endpoint, endpointAction); err == nil { + if req.Method == api.GET { + sendResponse(cachedData.StatusCode, cachedData.Data, rw, req, route.Endpoint, endpointAction, cachedData.ContentType, cachedData.File) + return true + } + + // Invalidate the cache if a modification, deletion or addition was made to this endpoint + cachedData.Invalidate() + } + + return false +} + +func respond(resp *api.Response, rw http.ResponseWriter, req *http.Request, endpoint, endpointAction string) { + if resp.StatusCode == 0 { + resp.StatusCode = http.StatusInternalServerError + sendMessageResponse(resp.StatusCode, api.StatusText(resp.StatusCode), rw, req, endpoint, endpointAction) + } else if len(resp.ErrorMessage) > 0 { + sendMessageResponse(resp.StatusCode, resp.ErrorMessage, rw, req, endpoint, endpointAction) + } else { + if len(resp.ContentType) == 0 { + resp.ContentType = api.ContentJSON + } + + sendResponse(resp.StatusCode, resp.Content, rw, req, endpoint, endpointAction, resp.ContentType, resp.File) + + // Try caching the data only if a GET request was made + go func(resp *api.Response, req *http.Request, endpoint string) { + if req.Method == api.GET && cache.Status == cache.StatusON { + cacheResponse(resp, endpoint, endpointAction) + } + }(resp, req, endpoint) + } +} + +func cacheResponse(resp *api.Response, endpoint, endpointAction string) { + if !(resp.StatusCode >= 200 && resp.StatusCode < 300) || len(resp.Content) == 0 { + return + } + + cacheEntity := &cache.Cache{ + Key: endpoint, + DataKey: endpointAction, + Data: resp.Content, + StatusCode: resp.StatusCode, + ContentType: resp.ContentType, + File: resp.File, + } + + cacheEntity.Cache() +} + +func generateRequest(req *http.Request, rw http.ResponseWriter, route *config.Route, endpointAction string, userIdentity *identity.Identity) *api.Request { + statusCode, err := filter.ParseRequestContent(req) + if err != nil { + sendMessageResponse(statusCode, err.Error(), rw, req, route.Endpoint, endpointAction) + return nil + } + + body, err := ioutil.ReadAll(req.Body) + if err != nil { + sendMessageResponse(http.StatusBadRequest, err.Error(), rw, req, route.Endpoint, endpointAction) + return nil + } + + request := &api.Request{ + Header: req.Header, + Form: req.Form, + ContentLength: req.ContentLength, + Body: body, + Identity: userIdentity, + } + + return request +} diff --git a/models/models.go b/models/models.go deleted file mode 100644 index a1762d6..0000000 --- a/models/models.go +++ /dev/null @@ -1,25 +0,0 @@ -package models - -import ( - "encoding/json" -) - -// Constants used for JSON serializations -const ( - jsonPrefix = "" - jsonIndent = " " -) - -type Modeler interface { - PopConstrains() -} - -// Create the JSON representation of a model -func SerializeJson(target interface{}) ([]byte, error) { - return json.MarshalIndent(target, jsonPrefix, jsonIndent) -} - -// Deserialize a JSON representation into a model -func DeserializeJson(jsonData []byte, target interface{}) error { - return json.Unmarshal(jsonData, target) -} diff --git a/models/user.go b/models/user.go deleted file mode 100644 index 41e7355..0000000 --- a/models/user.go +++ /dev/null @@ -1,76 +0,0 @@ -package models - -import ( - "gopkg.in/mgo.v2/bson" - "gost/dbmodels" -) - -// Struct representing an user account. This is a database dbmodels -type User struct { - Id bson.ObjectId `json:"id"` - - Email string `json:"email"` - Password string `json:"password"` - AccountType int `json:"accountType"` - Token string `json:"token"` - - FirstName string `json:"firstName"` - MiddleName string `json:"middleName"` - LastName string `json:"lastName"` - CompanyName string `json:"companyName"` - Sex rune `json:"sex"` - Country string `json:"country"` - State string `json:"state"` - City string `json:"city"` - Address string `json:"address"` - PostalCode int `json:"postalCode"` - Picture string `json:"picture"` -} - -func (user *User) PopConstrains() { - // Nothing to do here for now -} - -func (user *User) Expand(dbUser *dbmodels.User) { - user.Id = dbUser.Id - user.Email = dbUser.Email - user.Password = dbUser.Password - user.AccountType = dbUser.AccountType - user.Token = dbUser.Token - user.FirstName = dbUser.FirstName - user.MiddleName = dbUser.MiddleName - user.LastName = dbUser.LastName - user.CompanyName = dbUser.CompanyName - user.Sex = dbUser.Sex - user.Country = dbUser.Country - user.State = dbUser.State - user.City = dbUser.City - user.Address = dbUser.Address - user.PostalCode = dbUser.PostalCode - user.Picture = dbUser.Picture - - user.PopConstrains() -} - -func (user *User) Collapse() *dbmodels.User { - dbUser := dbmodels.User{ - Id: user.Id, - Email: user.Email, - Password: user.Password, - Token: user.Token, - AccountType: user.AccountType, - FirstName: user.FirstName, - MiddleName: user.MiddleName, - LastName: user.LastName, - CompanyName: user.CompanyName, - Sex: user.Sex, - Country: user.Country, - State: user.State, - City: user.City, - Address: user.Address, - PostalCode: user.PostalCode, - Picture: user.Picture, - } - - return &dbUser -} diff --git a/models/user_session.go b/models/user_session.go deleted file mode 100644 index 139fc3a..0000000 --- a/models/user_session.go +++ /dev/null @@ -1,43 +0,0 @@ -package models - -import ( - "gopkg.in/mgo.v2/bson" - "gost/dbmodels" - "gost/service/userservice" - "time" -) - -type UserSession struct { - Id bson.ObjectId `json:"id"` - - User User `json:"user"` - Token string `json:"token"` - ExpireDate time.Time `json:"expireDate"` -} - -func (userSession *UserSession) PopConstrains() { - dbUser, err := userservice.GetUser(userSession.User.Id) - if err == nil { - userSession.User.Expand(dbUser) - } -} - -func (userSession *UserSession) Expand(dbUserSession *dbmodels.UserSession) { - userSession.Id = dbUserSession.Id - userSession.User.Id = dbUserSession.UserId - userSession.Token = dbUserSession.Token - userSession.ExpireDate = dbUserSession.ExpireDate - - userSession.PopConstrains() -} - -func (userSession *UserSession) Collapse() *dbmodels.UserSession { - dbUserSession := dbmodels.UserSession{ - Id: userSession.Id, - UserId: userSession.User.Id, - Token: userSession.Token, - ExpireDate: userSession.ExpireDate, - } - - return &dbUserSession -} diff --git a/orm/dbmodels/dbmodels.go b/orm/dbmodels/dbmodels.go new file mode 100755 index 0000000..1c6a212 --- /dev/null +++ b/orm/dbmodels/dbmodels.go @@ -0,0 +1,6 @@ +package dbmodels + +// Objecter is an interface for defining a method for comparing two entities +type Objecter interface { + Equal(obj Objecter) bool +} diff --git a/dbmodels/transaction.go b/orm/dbmodels/transaction.go old mode 100644 new mode 100755 similarity index 58% rename from dbmodels/transaction.go rename to orm/dbmodels/transaction.go index e003919..d259c26 --- a/dbmodels/transaction.go +++ b/orm/dbmodels/transaction.go @@ -1,20 +1,18 @@ package dbmodels import ( - "gopkg.in/mgo.v2/bson" + "gost/util" "time" -) -const ( - CASH_TRANSACTION_TYPE = 0 - CARD_TRANSACTION_TYPE = 1 + "gopkg.in/mgo.v2/bson" ) +// Transaction is a struct representing transactions between users type Transaction struct { - Id bson.ObjectId `bson:"_id,omitempty" json:"id"` + ID bson.ObjectId `bson:"_id,omitempty" json:"id"` - PayerId bson.ObjectId `bson:"payerId,omitempty" json:"payerId"` - ReceiverId bson.ObjectId `bson:"receiverId,omitempty" json:"receiverId"` + PayerID bson.ObjectId `bson:"payerId,omitempty" json:"payerId"` + ReceiverID bson.ObjectId `bson:"receiverId,omitempty" json:"receiverId"` PaymentPortal string `bson:"paymentPortal,omitempty" json:"paymentPortal"` PaymentToken string `bson:"paymentToken,omitempty" json:"paymentToken"` @@ -25,18 +23,19 @@ type Transaction struct { Date time.Time `bson:"date,omitempty" json:"date"` } -func (transaction *Transaction) Equal(obj Object) bool { +// Equal compares two Transaction objects. Implements the Objecter interface +func (transaction *Transaction) Equal(obj Objecter) bool { otherTransaction, ok := obj.(*Transaction) if !ok { return false } switch { - case transaction.Id != otherTransaction.Id: + case transaction.ID != otherTransaction.ID: return false - case transaction.PayerId != otherTransaction.PayerId: + case transaction.PayerID != otherTransaction.PayerID: return false - case transaction.ReceiverId != otherTransaction.ReceiverId: + case transaction.ReceiverID != otherTransaction.ReceiverID: return false case transaction.Type != otherTransaction.Type: return false @@ -44,7 +43,7 @@ func (transaction *Transaction) Equal(obj Object) bool { return false case transaction.Currency != otherTransaction.Currency: return false - case !transaction.Date.Truncate(time.Millisecond).Equal(otherTransaction.Date.Truncate(time.Millisecond)): + case !util.CompareDates(transaction.Date, otherTransaction.Date): return false } diff --git a/models/transaction.go b/orm/models/transaction.go old mode 100644 new mode 100755 similarity index 55% rename from models/transaction.go rename to orm/models/transaction.go index caa18c9..a71e36f --- a/models/transaction.go +++ b/orm/models/transaction.go @@ -1,17 +1,25 @@ package models import ( - "gopkg.in/mgo.v2/bson" - "gost/dbmodels" - "gost/service/userservice" + "gost/auth/identity" + "gost/orm/dbmodels" "time" + + "gopkg.in/mgo.v2/bson" ) +// Constants representing the type of transaction +const ( + TransactionTypeCash = iota + TransactionTypeCard = iota +) + +// Transaction is a struct representing transactions between users type Transaction struct { - Id bson.ObjectId `json:"id"` + ID bson.ObjectId `json:"id"` - Payer User `json:"payer"` - Receiver User `json:"receiver"` + Payer identity.ApplicationUser `json:"payer"` + Receiver identity.ApplicationUser `json:"receiver"` PaymentPortal string `json:"paymentPortal"` PaymentToken string `json:"paymentToken"` @@ -22,37 +30,27 @@ type Transaction struct { Date time.Time `json:"date"` } -func (transaction *Transaction) PopConstrains() { - dbPayer, err := userservice.GetUser(transaction.Payer.Id) - if err != nil { - transaction.Payer.Expand(dbPayer) - } - - dbReceiver, err := userservice.GetUser(transaction.Receiver.Id) - if err != nil { - transaction.Receiver.Expand(dbReceiver) - } -} - +// Expand copies the dbmodels.Transaction to a Transaction expands all +// the components by fetching them from the database func (transaction *Transaction) Expand(dbTransaction *dbmodels.Transaction) { - transaction.Id = dbTransaction.Id - transaction.Payer.Id = dbTransaction.PayerId - transaction.Receiver.Id = dbTransaction.ReceiverId + transaction.ID = dbTransaction.ID + transaction.Payer.ID = dbTransaction.PayerID + transaction.Receiver.ID = dbTransaction.ReceiverID transaction.PaymentPortal = dbTransaction.PaymentPortal transaction.PaymentToken = dbTransaction.PaymentToken transaction.Type = dbTransaction.Type transaction.Ammount = dbTransaction.Ammount transaction.Currency = dbTransaction.Currency transaction.Date = dbTransaction.Date - - transaction.PopConstrains() } +// Collapse coppies the Transaction to a dbmodels.Transaction user and +// only keeps the unique identifiers from the inner components func (transaction *Transaction) Collapse() *dbmodels.Transaction { dbTransaction := dbmodels.Transaction{ - Id: transaction.Id, - PayerId: transaction.Payer.Id, - ReceiverId: transaction.Receiver.Id, + ID: transaction.ID, + PayerID: transaction.Payer.ID, + ReceiverID: transaction.Receiver.ID, PaymentPortal: transaction.PaymentPortal, PaymentToken: transaction.PaymentToken, Type: transaction.Type, diff --git a/security/cipher.go b/security/cipher.go new file mode 100755 index 0000000..ea13342 --- /dev/null +++ b/security/cipher.go @@ -0,0 +1,79 @@ +package security + +import ( + "crypto/rand" + "crypto/rsa" + "gost/util" + "log" + + "github.com/square/go-jose" +) + +const privateKeyFile = "config/enc.cfg" + +var privateKey = new(rsa.PrivateKey) +var encrypter jose.Encrypter + +// Encrypt encrypts a byte slice using RSA with a private key +func Encrypt(data []byte) ([]byte, error) { + object, err := encrypter.Encrypt(data) + if err != nil { + return nil, err + } + + encryptedString := object.FullSerialize() + return []byte(encryptedString), nil +} + +// Decrypt decrypts a byte slice using RSA with a private key +func Decrypt(data []byte) ([]byte, error) { + object, err := jose.ParseEncrypted(string(data)) + if err != nil { + return nil, err + } + + decryptedData, err := object.Decrypt(privateKey) + if err != nil { + return nil, err + } + + return decryptedData, nil +} + +// GeneratePrivateKey generates and prints in the terminal/log the byte array +// containing a private RSA key serialized as a JSON value +func GeneratePrivateKey(printInLog bool) []byte { + priv, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + panic(err) + } + + data, err := util.SerializeJSON(priv) + if err != nil { + panic(err) + } + + if printInLog { + log.Println(data) + } + + return data +} + +// InitCipherModule initializes the components used for server-side encryption +func InitCipherModule() { + key, err := util.Decode(encodedPrivateKey) + if err != nil { + panic(err) + } + + err = util.DeserializeJSON(key, privateKey) + if err != nil { + panic(err) + } + + encrypter, err = jose.NewEncrypter(jose.RSA_OAEP, jose.A128GCM, &privateKey.PublicKey) + if err != nil { + panic(err) + } +} diff --git a/security/key.go b/security/key.go new file mode 100755 index 0000000..e7f784d --- /dev/null +++ b/security/key.go @@ -0,0 +1,3 @@ +package security + +var encodedPrivateKey = []byte(`ewogICJOIjogMjU1ODYwMzYzMDI5NTg0NzY4NDM3NTE0MjExMDEyNjAxMjU1MjQxNDk0MDM5NDIxNjk0ODI1NTc1MDAwNjc3NTk0MTQ5MzA3OTQyMzQ1Nzg0ODY2OTE4NjE4ODU2MzU1OTM3NTk0Njk1OTEyMjg1MTYyMzU0Nzk2NTUxODcxNjQwNTE2Nzk3Mzc4OTk3Nzg1MTAwMDk1MjYxMDAwNDM5MjQxMjAyNTI0Mjc2NzA1Mjg3NTI4MDM4MzE5MjA0NjA1MjI3MDc5MDQ4MTUzNTUzMTM2NzcxODY0NjM0MTIxNTQ5OTI1NDQxNDk4NTcxMTQzMDQ3NzI4NTIxNjA1NDMyMzMwMjEzNTI5MTY1MzE3MTc3ODk3NDUzNjQzOTIxNzkyNzA1Mzc3NjA0OTYzMTE4ODM2MjU5ODUzMTA4MjUwNDg3MTQwNDc2OTAyMDQ5MDA4NTAzOTA2MjE0NzI5NTQ3MTA5Nzg2MjQzODg2MjY1ODQ5NzU4OTc3NjgzNTgyNjcxMTc1MjUyNjc2NTA4MDU0NjkyNTc3NjQwNzM3MzYxMDgyMzk1MzUwMjE5NDI3MDc5NzcwOTA0NzY1NDY5ODU0NzcxOTk2MDE3OTkyODMzMDIxNjU0NzQyMTQxMjM3NzAzMDI5NDcwNzg2MDk4NzIzOTY1NzI5Njg0ODExNTQzMTk2NDk2MjMzNDI1NTIxNjUyMzkwOTkwNDUxOTI1ODE2MTMxODAxMjgxOTkyNzcwMjY4MjY4Njg2NzExMjYzNzYyMDE3NTY1OTY1MTM0MzI3MDExMzk4NjEyNjkzNTE2MTAwODE3ODUzNDMsCiAgIkUiOiA2NTUzNywKICAiRCI6IDMxNTMzMDkwNTAxMzk1NDg5MTg0Mjc0NTY2NzY5MTM0NjkyNDQyMjE2NTcwMTI2OTM2MzczNDQwNDg4MjgxMDE1NzMwNzE2MzkzMDM0ODc5NjAwMzc5OTYzNzUzMzQyMTg2OTc3NjIxMTc0MDQ3MTM3NTkxNTg0NzgwMzQ0OTUzODQ5Nzk3NDAzNDU0MDA0NzY0NTM3MTIyODkzOTQ2MTMzNTAwMDUwMTc5MDI3ODU2MTMyNjI2ODQ0NDQyMzA4ODcwMDkwNDQ0NjYzODczMDU5Mjc1MDEwMzA5NDQwNDY1MDYxNjg3MTUzNTc2NzQ0Nzc2MTc5OTE3NzQxMjMxMzE4NjYxNzQzMzY3Mzg0MzMwMTYyNzc0MjA1MTM1MzYzNTMwMjM5OTM5NTA0MjA1NDIzNTA4MDE4MDEyOTE5NzYxNjcwMzY5MDkzMTM3Nzc5MjM5MzQ1NTY1MTY2OTE4MjYzNjMxOTM0OTkxMDkyMzQxMTU4ODYzNzUxODg1MTA2NTgzNDA3NjQ1NTk1NDg2MTEwNjY2OTY3MTY3ODQxNjE4NDAyNzM4NzQxMjEyMjcyMDQ1MDU0MTE1NzM4NDI5ODEyNDk4ODMxNzkyMjQ5MzA4OTA0NzU1MDgxMDMyNzE3NjIwNDAzMTA4MTQ1MTU1NDc5ODkzMzczMjIyODg2MDk5OTIzNjU1MTQ1MjE2MDA1ODc4Mjg2NTE5MTkwMjk5OTAxMTUxMzQ3NTgxMjk3NDUxMDA4MzA4NDA4NjQzNDAyMDE5MzEyNDMyMzk1MDc1MDg5NzQ1Mzk2NTgxNjY3MTA5NDA2MjMzMTMsCiAgIlByaW1lcyI6IFsKICAgIDE2NTk4NjIxNjE1OTk5NDQyNTE1NzE3NjUzMTc4Mzk2NjM5MjI1NDY2MzQxMTI4MTc5NTQ0NjE3NzIzOTAzNDE4ODMwODg4NTgwOTUwOTk5Njk5NjE0MTMzODQ2MzgwMTk4ODQxNTI3NDUyOTc3OTYwNDczNzUwMDQ2NTAyNzc3ODg2NDQ3ODc5NTI1OTU4NDU4MDUzOTI0OTYwMTgxOTA2OTg1MDQxNDIzNTMyMDc1MzAzODY3ODU1NTg3NTMwMTY0NDI5NDcwNzA1NTgyNTA0MTg0NjQ5MTk2NTA1NjI5OTIzMTUwNTExMTk1NTg4Mjc2MDk2NDQ4OTc3NTA3MjIxODg0OTU4NDI0Nzg2MjA4MzI4MTA5MjQzNzQ4NzMzMTgwOTc5NDkxNzQ3ODE1NTA1NjQ3MSwKICAgIDE1NDE0NTU0ODMxNjQ3MDEzNjU5NDQ3NDgxNDA1ODI2NTM3OTM0NDAyNzA2NDkxMTA2NTM3MjkxNTg3MTEwNTQwMzIzNTUzNjY2MTc2NTU0NzUwMDU2MjQzOTk3NTE4NzM0NjQ0ODYyMDg0MzcxNjA4NjE3Mjk3OTcxMTQwMjczODQ4MzYzOTk0MzY1Nzk3OTgxMDkxOTk0ODk3ODgxOTk1OTcyMTU2OTYxNjUxMzg0NzM5NTE1NTkxNjU2MjMwNDIzMjQzMzIxODUxMTQzMjY0NjUyOTQ2MzQyNjkwOTk4NDg2MjQ5MDU1NjM4OTcyNTc5NTI4NDM2NjQxNDIzMTY2NDcwMTExMDQ5OTc4NzczOTY5MDUzNzA1MjcwODQ2MzkzNzg3MDcxMTAwMDIwNTA5NTgzMwogIF0sCiAgIlByZWNvbXB1dGVkIjogewogICAgIkRwIjogMjIyMTk0NjQ5NDkxMzc2MDMwNjI0NTE4OTMwMjc0NjEzOTA2NTMzNzM4NTE1MjE2NjI0NDE4MTYyNzA0NzM4ODM5NzQ0NTQ5Njc1MjcyMTY3NDI0MDczMTEzMjU1NTU1NjE2NTc3OTk0NjM2NTgwMzI0NDUyMTU1NTEyMTA1OTQwNzYyOTM4ODY5NzcwMTA0NzUzODE0MDAzODA4MDQxMDYwNzQzOTU4OTk4MTk0NzU1MDg2MTg0NDQwOTU2MTA0Mzg3NjU4NDkyOTEyNTE1NTM5NjM3MDQwNzU3MDQzOTQ2NjQ5Njc3OTQ0ODUzODkzMTM4NTIzNTAxMDQ0NzA4ODc4MzM4NTU3MjQzNDY0NjE2NDE4OTczMDkyMTU4MDcyODM5MzYyNjIxMjcyODQ2NTMxMDMsCiAgICAiRHEiOiAyNjk2ODQ3MzY0MDc5MjkzNTA3Nzc3NjY0ODU4MDA3MDM1NDc1NDY5NzU2NTEzNTI3MTMwNTc2Mzk3MTE2Mjc3MTQ2NDk1MzU4OTAyMzA1MjEzMzAxNTY4NDgzMDc5MDIxMTg1NTU5MTI5MzM3NzAwMjk3NjMyNDYwMDkyNjg2MjY3OTkxMjM0ODY1Nzc0MTM3NTI4NDMxNDczODEwNDQyNDM0MzAwNDk3NzY5MTE5Mzg5NDAyNzE1NjI1MjI0NDk5NjQwMDE5MDQ3OTQ1NTY3NzMyODk0MTMyNTU1NTc5MTE3ODAxMTE0OTcyNTU2ODcwMTE3MjMyMzE5NjE0MjMzNTg5MzY2MjA5OTEyODIyMTA1NTE1NTA3MDM2MjYyMzM2NTYwNDU1MjczMDk1MTI0MzI0OSwKICAgICJRaW52IjogMTI3Mjg5MjMyNDg3NjI4MTMxNjEyNjExMTM2NTE5ODczODkyMjc0NzMyMjgzMzc2MTMzOTgyODE1MjYyMDU0MzEyNzk0NTIwODYyOTc1MDg2NzIzNzQ2NjQ1Nzc2ODA0ODM2ODYxOTMyODgyMTIwMDQzMDU1MTM5MTY0MDU0NjQ5NzI3NTgyODczNjIwOTA3NTM0MDA0MzIzMzIzNTAxNDc2MjkwODMxOTUzNjk2MTIyMzY1OTM0MDMzNTcwMTQ4MDUxMjYzNzQ0NjIwNjUxOTU2MjI4MTE3NTE0MjU5OTA1NTI5ODA3NjgwMjM1NTIzNjQ4MDEzNjQxMjg0ODU1MDE5OTE2MTY2ODYxNjQ2OTg5NDQzMDgwMDI1MDA2NjM3NDEwNzM1MTcwOTcwNzIzODU0NTY5LAogICAgIkNSVFZhbHVlcyI6IFtdCiAgfQp9`) diff --git a/servers/http_server.go b/servers/http_server.go old mode 100644 new mode 100755 index e622e68..e3d8a2c --- a/servers/http_server.go +++ b/servers/http_server.go @@ -9,34 +9,36 @@ import ( ) const ( - CERT_FILE = "gost.crt" - KEY_FILE = "gost.key" + httpsCertFile = "gost.crt" + httpsKeyFile = "gost.key" ) +// StartHTTPServer starts a HTTP server that listens for requests func StartHTTPServer() { - http.HandleFunc(config.ApiInstance, httphandle.ApiHandler) + http.HandleFunc(config.APIInstance, httphandle.RequestHandler) server := &http.Server{ - Addr: config.HttpServerAddress, + Addr: config.HTTPServerAddress, ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, MaxHeaderBytes: 1 << 20, } - log.Println("HTTP Server STARTED! Listening at:", config.HttpServerAddress+config.ApiInstance) + log.Println("HTTP Server STARTED! Listening at:", config.HTTPServerAddress+config.APIInstance) log.Fatal(server.ListenAndServe()) } +// StartHTTPSServer starts a HTTPS server that listens for requests func StartHTTPSServer() { - http.HandleFunc(config.ApiInstance, httphandle.ApiHandler) + http.HandleFunc(config.APIInstance, httphandle.RequestHandler) server := &http.Server{ - Addr: config.HttpServerAddress, + Addr: config.HTTPServerAddress, ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, MaxHeaderBytes: 1 << 20, } - log.Println("HTTPS Server STARTED! Listening at:", config.HttpServerAddress+config.ApiInstance) - log.Fatal(server.ListenAndServeTLS(CERT_FILE, KEY_FILE)) + log.Println("HTTPS Server STARTED! Listening at:", config.HTTPServerAddress+config.APIInstance) + log.Fatal(server.ListenAndServeTLS(httpsCertFile, httpsKeyFile)) } diff --git a/service/service.go b/service/service.go old mode 100644 new mode 100755 index d84851f..1063067 --- a/service/service.go +++ b/service/service.go @@ -1,36 +1,33 @@ -// Package which contains all the services necessary for interacting -// with all the collections(tables) in the database. +// Package service contains all the services necessary for interacting +// with all the collections(tables) in the database. // // Each service file should be written in it's own file and should represent only one dbmodels package service import ( "errors" - "gopkg.in/mgo.v2" "gost/config" "log" -) -type Service struct { - session *mgo.Session -} + "gopkg.in/mgo.v2" +) -var mongoDBService *Service = &Service{} +var mongoDbSession *mgo.Session var ( - NoIdSpecifiedError = errors.New("No Id was specified for the entity") + // ErrNoIDSpecified says that no ID was specified for a fetch operation + ErrNoIDSpecified = errors.New("No Id was specified for the entity") ) -// Initialize a main session to the +// InitDbService initializes the connection (known as session) parameters to the database func InitDbService() { - url := config.DbConnectionString - if url == "" { + if config.DbConnectionString == "" { log.Fatal("Database error: No connection string provided") } - if mongoDBService.session == nil { + if mongoDbSession == nil { var err error - mongoDBService.session, err = mgo.Dial(url) + mongoDbSession, err = mgo.Dial(config.DbConnectionString) if err != nil { log.Fatalf("Can't connect to mongo, go error: %v\n", err) @@ -38,16 +35,13 @@ func InitDbService() { } } -// Close the mongodb service +// CloseDbService closes the current mongodb session func CloseDbService() { - mongoDBService.session.Close() + mongoDbSession.Close() } -// All connections should be stateless, so this method always -// returns a pointer to a new session. After each session is used, -// it should be closed in order to dump data from memory correctly. -// To avoid forgetting to close the session, always write: *defer session.Close()* -// right after getting it through this method +// Connect creates the connection to the database. +// To avoid forgetting to close the session, always write: *defer session.Close()* right after getting it through this method func Connect(collectionName string) (*mgo.Session, *mgo.Collection) { - return mongoDBService.session.Copy(), mongoDBService.session.DB(config.DbName).C(collectionName) + return mongoDbSession.Copy(), mongoDbSession.DB(config.DbName).C(collectionName) } diff --git a/service/service_test.go b/service/service_test.go old mode 100644 new mode 100755 index 8269d3e..623ffb3 --- a/service/service_test.go +++ b/service/service_test.go @@ -1,12 +1,12 @@ package service import ( - "gost/config" + testconfig "gost/tests/config" "testing" ) func TestServiceBase(t *testing.T) { - config.InitTestsDatabase() + testconfig.InitTestsDatabase() InitDbService() sess, col := Connect("testCollection") diff --git a/service/transactionservice/transactions_service_CRUD.go b/service/transactionservice/transactions_service_CRUD.go old mode 100644 new mode 100755 index dff9883..1fbf883 --- a/service/transactionservice/transactions_service_CRUD.go +++ b/service/transactionservice/transactions_service_CRUD.go @@ -1,19 +1,21 @@ package transactionservice import ( - "gopkg.in/mgo.v2/bson" - "gost/dbmodels" + "gost/orm/dbmodels" "gost/service" + + "gopkg.in/mgo.v2/bson" ) -const CollectionName = "transactions" +const collectionName = "transactions" +// CreateTransaction adds a new Transaction to the database func CreateTransaction(transaction *dbmodels.Transaction) error { - session, collection := service.Connect(CollectionName) + session, collection := service.Connect(collectionName) defer session.Close() - if transaction.Id == "" { - transaction.Id = bson.NewObjectId() + if transaction.ID == "" { + transaction.ID = bson.NewObjectId() } err := collection.Insert(transaction) @@ -21,40 +23,44 @@ func CreateTransaction(transaction *dbmodels.Transaction) error { return err } +// UpdateTransaction updates an existing Transaction in the database func UpdateTransaction(transaction *dbmodels.Transaction) error { - session, collection := service.Connect(CollectionName) + session, collection := service.Connect(collectionName) defer session.Close() - if transaction.Id == "" { - return service.NoIdSpecifiedError + if transaction.ID == "" { + return service.ErrNoIDSpecified } - err := collection.UpdateId(transaction.Id, transaction) + err := collection.UpdateId(transaction.ID, transaction) return err } -func DeleteTransaction(transactionId bson.ObjectId) error { - session, collection := service.Connect(CollectionName) +// DeleteTransaction removes a Transaction from the database +func DeleteTransaction(transactionID bson.ObjectId) error { + session, collection := service.Connect(collectionName) defer session.Close() - err := collection.RemoveId(transactionId) + err := collection.RemoveId(transactionID) return err } -func GetTransaction(transactionId bson.ObjectId) (*dbmodels.Transaction, error) { - session, collection := service.Connect(CollectionName) +// GetTransaction retrieves an Transaction from the database, based on its ID +func GetTransaction(transactionID bson.ObjectId) (*dbmodels.Transaction, error) { + session, collection := service.Connect(collectionName) defer session.Close() transaction := dbmodels.Transaction{} - err := collection.FindId(transactionId).One(&transaction) + err := collection.FindId(transactionID).One(&transaction) return &transaction, err } +// GetAllTransactions retrieves all the existing Transaction entities in the database func GetAllTransactions() ([]dbmodels.Transaction, error) { - session, collection := service.Connect(CollectionName) + session, collection := service.Connect(collectionName) defer session.Close() var transactions []dbmodels.Transaction @@ -63,8 +69,9 @@ func GetAllTransactions() ([]dbmodels.Transaction, error) { return transactions, err } +// GetAllTransactionsLimited retrieves the first X Transaction entities from the database, where X is the specified limit func GetAllTransactionsLimited(limit int) ([]dbmodels.Transaction, error) { - session, collection := service.Connect(CollectionName) + session, collection := service.Connect(collectionName) defer session.Close() var transactions []dbmodels.Transaction diff --git a/service/transactionservice/transactions_service_CRUD_test.go b/service/transactionservice/transactions_service_CRUD_test.go old mode 100644 new mode 100755 index 07a6331..0d2e8d0 --- a/service/transactionservice/transactions_service_CRUD_test.go +++ b/service/transactionservice/transactions_service_CRUD_test.go @@ -1,12 +1,14 @@ package transactionservice import ( - "gopkg.in/mgo.v2/bson" - "gost/config" - "gost/dbmodels" + "gost/orm/dbmodels" + "gost/orm/models" "gost/service" + testconfig "gost/tests/config" "testing" "time" + + "gopkg.in/mgo.v2/bson" ) func TestTransactionCRUD(t *testing.T) { @@ -25,7 +27,7 @@ func TestTransactionCRUD(t *testing.T) { } func setUpTransactionsTest(t *testing.T) { - config.InitTestsDatabase() + testconfig.InitTestsDatabase() service.InitDbService() if recover() != nil { @@ -34,7 +36,7 @@ func setUpTransactionsTest(t *testing.T) { } func tearDownTransactionsTest(t *testing.T, transaction *dbmodels.Transaction) { - err := DeleteTransaction(transaction.Id) + err := DeleteTransaction(transaction.ID) if err != nil { t.Fatal("The transaction document could not be deleted!") @@ -43,10 +45,10 @@ func tearDownTransactionsTest(t *testing.T, transaction *dbmodels.Transaction) { func createTransaction(t *testing.T, transaction *dbmodels.Transaction) { *transaction = dbmodels.Transaction{ - Id: bson.NewObjectId(), - PayerId: bson.NewObjectId(), - ReceiverId: bson.NewObjectId(), - Type: dbmodels.CASH_TRANSACTION_TYPE, + ID: bson.NewObjectId(), + PayerID: bson.NewObjectId(), + ReceiverID: bson.NewObjectId(), + Type: models.TransactionTypeCash, Ammount: 6469.1264, Currency: "RON", Date: time.Now().Local(), @@ -60,9 +62,9 @@ func createTransaction(t *testing.T, transaction *dbmodels.Transaction) { } func changeAndUpdateTransaction(t *testing.T, transaction *dbmodels.Transaction) { - transaction.PayerId = bson.NewObjectId() - transaction.ReceiverId = bson.NewObjectId() - transaction.Type = dbmodels.CARD_TRANSACTION_TYPE + transaction.PayerID = bson.NewObjectId() + transaction.ReceiverID = bson.NewObjectId() + transaction.Type = models.TransactionTypeCard transaction.Currency = "USD" err := UpdateTransaction(transaction) @@ -73,7 +75,7 @@ func changeAndUpdateTransaction(t *testing.T, transaction *dbmodels.Transaction) } func verifyTransactionCorresponds(t *testing.T, transaction *dbmodels.Transaction) { - dbtransaction, err := GetTransaction(transaction.Id) + dbtransaction, err := GetTransaction(transaction.ID) if err != nil || dbtransaction == nil { t.Error("Could not fetch the transaction document from the database!") diff --git a/service/userloginservice/user_login_service.go b/service/userloginservice/user_login_service.go deleted file mode 100644 index b68f5eb..0000000 --- a/service/userloginservice/user_login_service.go +++ /dev/null @@ -1,67 +0,0 @@ -package userloginservice - -import ( - "gopkg.in/mgo.v2/bson" - "gost/dbmodels" - "gost/service" - "time" -) - -const CollectionName = "user_sessions" - -func CreateUserSession(userSession *dbmodels.UserSession) error { - session, collection := service.Connect(CollectionName) - defer session.Close() - - if userSession.Id == "" { - userSession.Id = bson.NewObjectId() - } - - err := collection.Insert(userSession) - - return err -} - -func UpdateUserSession(userSession *dbmodels.UserSession) error { - session, collection := service.Connect(CollectionName) - defer session.Close() - - if userSession.Id == "" { - return service.NoIdSpecifiedError - } - - err := collection.UpdateId(userSession.Id, userSession) - - return err -} - -func DeleteUserSession(sessionId bson.ObjectId) error { - session, collection := service.Connect(CollectionName) - defer session.Close() - - err := collection.RemoveId(sessionId) - - return err -} - -func GetUserSession(token string) (*dbmodels.UserSession, error) { - session, collection := service.Connect(CollectionName) - defer session.Close() - - userSession := dbmodels.UserSession{} - err := collection.Find(bson.M{"token": token}).One(&userSession) - - return &userSession, err -} - -func DeleteExpiredSessionsForUser(userId bson.ObjectId) error { - session, collection := service.Connect(CollectionName) - defer session.Close() - - _, err := collection.RemoveAll(bson.M{ - "userId": userId, - "expireDate": bson.M{"$lte": time.Now().Local()}, - }) - - return err -} diff --git a/service/userloginservice/user_login_service_test.go b/service/userloginservice/user_login_service_test.go deleted file mode 100644 index bf8c3c0..0000000 --- a/service/userloginservice/user_login_service_test.go +++ /dev/null @@ -1,114 +0,0 @@ -package userloginservice - -import ( - "gopkg.in/mgo.v2/bson" - "gost/config" - "gost/dbmodels" - "gost/service" - "testing" - "time" -) - -func TestUserSessionCRUD(t *testing.T) { - userSession := &dbmodels.UserSession{} - - setUpUserSessionsTest(t) - defer tearDownUserSessionsTest(t, userSession) - - testCreateUserSession(t, userSession) - testVerifyUserSessionCorresponds(t, userSession) - - if t.Failed() { - return - } - - testChangeAndUpdateUserSession(t, userSession) - testVerifyUserSessionCorresponds(t, userSession) - - testDeleteExpiredUserSessions(t) -} - -func setUpUserSessionsTest(t *testing.T) { - config.InitTestsDatabase() - service.InitDbService() - - if recover() != nil { - t.Fatal("Test setup failed!") - } -} - -func tearDownUserSessionsTest(t *testing.T, userSession *dbmodels.UserSession) { - err := DeleteUserSession(userSession.Id) - - if err != nil { - t.Fatal("The user session document could not be deleted!") - } -} - -func testCreateUserSession(t *testing.T, userSession *dbmodels.UserSession) { - *userSession = dbmodels.UserSession{ - UserId: bson.NewObjectId(), - Token: "trh46rth46rth4r", - ExpireDate: time.Now().Local(), - } - - err := CreateUserSession(userSession) - - if err != nil { - t.Fatal("The user session document could not be created!") - } -} - -func testChangeAndUpdateUserSession(t *testing.T, userSession *dbmodels.UserSession) { - userSession.UserId = bson.NewObjectId() - userSession.Token = "a65g4as65as4g6as4ga" - userSession.ExpireDate = time.Date(2015, time.December, 12, 0, 0, 0, 0, time.UTC) - - err := UpdateUserSession(userSession) - - if err != nil { - t.Fatal("The user session document could not be updated!") - } -} - -func testVerifyUserSessionCorresponds(t *testing.T, userSession *dbmodels.UserSession) { - dbUserSession, err := GetUserSession(userSession.Token) - - if err != nil || dbUserSession == nil { - t.Error("Could not fetch the user session document from the database!") - } - - if !dbUserSession.Equal(userSession) { - t.Error("The user session document doesn't correspond with the document extracted from the database!") - } -} - -func testDeleteExpiredUserSessions(t *testing.T) { - userSession1 := &dbmodels.UserSession{ - Id: bson.NewObjectId(), - UserId: bson.NewObjectId(), - Token: "as7f6as8faf5aasf6721rqf", - ExpireDate: time.Now().Local().Add(-time.Hour * 150), - } - - userSession2 := &dbmodels.UserSession{ - Id: bson.NewObjectId(), - UserId: userSession1.UserId, - Token: "a68f4asg6546sgafas4f6a", - ExpireDate: time.Now().Local().Add(-time.Hour * 300), - } - - err1 := CreateUserSession(userSession1) - err2 := CreateUserSession(userSession2) - if err1 != nil || err2 != nil { - t.Fatal("Error creating expired user sessions!") - } - - DeleteExpiredSessionsForUser(userSession1.UserId) - _, err1 = GetUserSession(userSession1.Token) - _, err2 = GetUserSession(userSession2.Token) - - if err1 == nil && err2 == nil { - t.Fatal("The expired user sessions haven't been properly deleted!") - } -} diff --git a/service/userservice/users_service_CRUD.go b/service/userservice/users_service_CRUD.go deleted file mode 100644 index 836f1ee..0000000 --- a/service/userservice/users_service_CRUD.go +++ /dev/null @@ -1,74 +0,0 @@ -package userservice - -import ( - "gopkg.in/mgo.v2/bson" - "gost/dbmodels" - "gost/service" -) - -const CollectionName = "users" - -func CreateUser(user *dbmodels.User) error { - session, collection := service.Connect(CollectionName) - defer session.Close() - - if user.Id == "" { - user.Id = bson.NewObjectId() - } - - err := collection.Insert(user) - - return err -} - -func UpdateUser(user *dbmodels.User) error { - session, collection := service.Connect(CollectionName) - defer session.Close() - - if user.Id == "" { - return service.NoIdSpecifiedError - } - - err := collection.UpdateId(user.Id, user) - - return err -} - -func DeleteUser(userId bson.ObjectId) error { - session, collection := service.Connect(CollectionName) - defer session.Close() - - err := collection.RemoveId(userId) - - return err -} - -func GetUser(userId bson.ObjectId) (*dbmodels.User, error) { - session, collection := service.Connect(CollectionName) - defer session.Close() - - user := dbmodels.User{} - err := collection.FindId(userId).One(&user) - - return &user, err -} - -func GetAllUsers() ([]dbmodels.User, error) { - session, collection := service.Connect(CollectionName) - defer session.Close() - - var users []dbmodels.User - err := collection.Find(bson.M{}).All(&users) - - return users, err -} - -func GetAllUsersLimited(limit int) ([]dbmodels.User, error) { - session, collection := service.Connect(CollectionName) - defer session.Close() - - var users []dbmodels.User - err := collection.Find(bson.M{}).Limit(limit).All(&users) - - return users, err -} diff --git a/service/userservice/users_service_CRUD_test.go b/service/userservice/users_service_CRUD_test.go deleted file mode 100644 index 9b3ba01..0000000 --- a/service/userservice/users_service_CRUD_test.go +++ /dev/null @@ -1,92 +0,0 @@ -package userservice - -import ( - "gopkg.in/mgo.v2/bson" - "gost/config" - "gost/dbmodels" - "gost/service" - "testing" -) - -func TestUserCRUD(t *testing.T) { - user := &dbmodels.User{} - - setUpUsersTest(t) - defer tearDownUsersTest(t, user) - - createUser(t, user) - verifyUserCorresponds(t, user) - - if !t.Failed() { - changeAndUpdateUser(t, user) - verifyUserCorresponds(t, user) - } -} - -func setUpUsersTest(t *testing.T) { - config.InitTestsDatabase() - service.InitDbService() - - if recover() != nil { - t.Fatal("Test setup failed!") - } -} - -func tearDownUsersTest(t *testing.T, user *dbmodels.User) { - err := DeleteUser(user.Id) - - if err != nil { - t.Fatal("The user document could not be deleted!") - } -} - -func createUser(t *testing.T, user *dbmodels.User) { - *user = dbmodels.User{ - Id: bson.NewObjectId(), - Password: "CoddoPass", - AccountType: dbmodels.ADMINISTRATOR_ACCOUNT_TYPE, - FirstName: "Claudiu", - LastName: "Codoban", - Email: "test@tests.com", - Sex: 'M', - Country: "Romania", - State: "Hunedoara", - City: "Deva", - Address: "AddrTest", - PostalCode: 330099, - Picture: "ftp://pictLink", - Token: "as7f6as8faf5aasf6721rqf", - } - - err := CreateUser(user) - - if err != nil { - t.Fatal("The user document could not be created!") - } -} - -func changeAndUpdateUser(t *testing.T, user *dbmodels.User) { - user.Email = "testEmailCHanged@email.go" - user.Password = "ChangedPassword" - user.City = "Timisoara" - user.PostalCode = 12512521 - user.AccountType = dbmodels.CLIENT_ACCOUNT_TYPE - - err := UpdateUser(user) - - if err != nil { - t.Fatal("The user document could not be updated!") - } -} - -func verifyUserCorresponds(t *testing.T, user *dbmodels.User) { - dbuser, err := GetUser(user.Id) - - if err != nil || dbuser == nil { - t.Error("Could not fetch the user document from the database!") - } - - if !dbuser.Equal(user) { - t.Error("The user document doesn't correspond with the document extracted from the database!") - } -} diff --git a/tests/api_tests_aux.go b/tests/api_tests_aux.go old mode 100644 new mode 100755 index 3f863b5..9ffe40a --- a/tests/api_tests_aux.go +++ b/tests/api_tests_aux.go @@ -4,8 +4,9 @@ import ( "bytes" "gost/config" "gost/httphandle" - "gost/models" "gost/service" + testconfig "gost/tests/config" + "gost/util" "net/http" "net/http/httptest" "net/url" @@ -14,8 +15,9 @@ import ( "testing" ) -func PerformApiTestCall(endpointName, method string, expectedStatusCode int, urlParams url.Values, object interface{}, t *testing.T) *httptest.ResponseRecorder { - Url, err := generateApiUrl(endpointName, urlParams) +// PerformTestRequest does a HTTP request with test data on a specified endpoint +func PerformTestRequest(route, endpoint, method string, expectedStatusCode int, urlParams url.Values, object interface{}, t *testing.T) *httptest.ResponseRecorder { + generatedURL, err := generateEndpointURL(route, endpoint, urlParams) if err != nil { t.Error(err.Error()) } @@ -24,59 +26,63 @@ func PerformApiTestCall(endpointName, method string, expectedStatusCode int, url // Do nothing if no object is specified var jsonData []byte if object != nil { - jsonData, err = models.SerializeJson(object) + jsonData, err = util.SerializeJSON(object) if err != nil { t.Fatal(err.Error()) } } - req, err := http.NewRequest(method, Url.String(), bytes.NewBuffer(jsonData)) + req, err := http.NewRequest(method, generatedURL.String(), bytes.NewBuffer(jsonData)) if err != nil { t.Fatal(err.Error()) } rw := httptest.NewRecorder() - httphandle.ApiHandler(rw, req) + httphandle.RequestHandler(rw, req) if rw.Code != expectedStatusCode { - t.Fatal("Response assertion failed! Needed:", expectedStatusCode, "Got:", rw.Code, "Message:", rw.Body.String()) + t.Fatal("Response assertion failed! Needed status:", expectedStatusCode, "Got:", rw.Code, "Message:", rw.Body.String()) } return rw } -func InitializeServerConfigurations(routeString string, apiInterface interface{}) { - config.InitTestsApp() - config.InitTestsDatabase() - config.InitTestsRoutes(routeString) +// InitializeServerConfigurations initializes the HTTP/HTTPS server used for unit testing +func InitializeServerConfigurations(apiInterface interface{}) { + testconfig.InitTestsApp() + + testconfig.InitTestsDatabase() + testconfig.InitTestsRoutes() service.InitDbService() - httphandle.SetApiInterface(apiInterface) + httphandle.RegisterEndpoints(apiInterface) runtime.GOMAXPROCS(2) } -func generateApiUrl(path string, params url.Values) (*url.URL, error) { +func generateEndpointURL(route, endpoint string, params url.Values) (*url.URL, error) { buffer := &bytes.Buffer{} - if !strings.Contains(config.HttpServerAddress, "http://") { + if !strings.Contains(config.HTTPServerAddress, "http://") { buffer.WriteString("http://") } - buffer.WriteString(config.HttpServerAddress) - buffer.WriteString(config.ApiInstance[0 : len(config.ApiInstance)-1]) - buffer.WriteString(path) + buffer.WriteString(config.HTTPServerAddress) + buffer.WriteString(config.APIInstance[0 : len(config.APIInstance)-1]) + buffer.WriteString(route) + buffer.WriteRune('/') + buffer.WriteString(endpoint) bufferString := buffer.String() bufferString = strings.Replace(bufferString, "[", "", 1) bufferString = strings.Replace(bufferString, "]", "", 1) - Url, err := url.Parse(bufferString) - if Url != nil && params != nil { - Url.RawQuery = params.Encode() + parsedURL, err := url.Parse(bufferString) + if parsedURL != nil && params != nil { + parsedURL.RawQuery = params.Encode() } - return Url, err + return parsedURL, err } diff --git a/tests/config/config_tests_aux.go b/tests/config/config_tests_aux.go new file mode 100755 index 0000000..1f2d6b4 --- /dev/null +++ b/tests/config/config_tests_aux.go @@ -0,0 +1,52 @@ +package config + +import ( + "encoding/json" + "gost/config" + "log" + "os" +) + +// All the api files are 3 levels deep into the folder hierarchy (ex: ./api/app/someapi/someapi.go), hence the ../../../ prefix +const routesFilePath = "../../../tests/config/test_routes.json" + +const ( + envApplicationName = "GOST_TESTAPP_NAME" + envAPIInstance = "GOST_TESTAPP_INSTANCE" + envHTTPServerAddress = "GOST_TESTAPP_HTTP" + envDatabaseName = "GOST_TESTAPP_DB_NAME" + envDatabaseConnection = "GOST_TESTAPP_DB_CONN" +) + +// InitTestsApp initializes the application used for testing +func InitTestsApp() { + config.ApplicationName = os.Getenv(envApplicationName) + config.APIInstance = os.Getenv(envAPIInstance) + config.HTTPServerAddress = os.Getenv(envHTTPServerAddress) +} + +// InitTestsDatabase initializes the connection parameters to the database used for testing +func InitTestsDatabase() { + dbName := os.Getenv(envDatabaseName) + dbConn := os.Getenv(envDatabaseConnection) + + if len(dbName) == 0 || len(dbConn) == 0 { + log.Fatal("Environment variables for the test database are not set!") + } + + config.DbName = dbName + config.DbConnectionString = dbConn +} + +// InitTestsRoutes initializez the routes used for testing the endpoints +func InitTestsRoutes() { + config.InitRoutes(routesFilePath) +} + +func deserializeRoutes(routesString []byte) { + err := json.Unmarshal(routesString, &config.Routes) + + if err != nil { + log.Fatal(err) + } +} diff --git a/tests/config/test_routes.json b/tests/config/test_routes.json new file mode 100755 index 0000000..09c9460 --- /dev/null +++ b/tests/config/test_routes.json @@ -0,0 +1,64 @@ +[ + { + "id": "TransactionsRoute", + "endpoint": "/transactions", + "actions": { + "CreateTransaction": { + "type": "POST", + "allowAnonymous": true + }, + "GetTransaction": { + "type": "GET", + "allowAnonymous": true + } + } + }, + { + "id": "AuthorizationRoute", + "endpoint": "/auth", + "actions": { + "ActivateAccount": { + "type": "GET", + "allowAnonymous": true + }, + "CreateSession": { + "type": "POST", + "allowAnonymous": true + }, + "GetAllSessions": { + "type": "GET", + "allowAnonymous": true + }, + "KillSession": { + "type": "POST", + "allowAnonymous": true + }, + "RequestResetPassword": { + "type": "GET", + "allowAnonymous": true + }, + "ResetPassword": { + "type": "GET", + "allowAnonymous": true + } + } + }, + { + "id": "ValuesRoute", + "endpoint": "/values", + "actions": { + "CreateAppUser": { + "type": "POST", + "allowAnonymous": true + }, + "Get": { + "type": "GET", + "allowAnonymous": true + }, + "GetAnonymous": { + "type": "GET", + "allowAnonymous": true + } + } + } +] \ No newline at end of file diff --git a/util/date.go b/util/date.go new file mode 100755 index 0000000..7bbb1ac --- /dev/null +++ b/util/date.go @@ -0,0 +1,47 @@ +package util + +import ( + "time" +) + +// Now retrieves the current date and time +func Now() time.Time { + return time.Now().Local() +} + +// NextDate uses the selected date and time to add a given duration to it, +// then returns the result +func NextDate(date time.Time, duration time.Duration) time.Time { + return date.Local().Add(duration) +} + +// NextDateFromNow gets the current date and time and then adds the given duration to it, +// then returns the result +func NextDateFromNow(duration time.Duration) time.Time { + return Now().Add(duration) +} + +// CompareDates checks whether two dates are identical or note. +// Both the date and the time are compared up to millisecond precision +func CompareDates(source, target time.Time) bool { + source = source.Local().Truncate(time.Millisecond) + target = target.Local().Truncate(time.Millisecond) + + return source.Equal(target) +} + +// IsDateExpired tells if the limit date and time are greater than the given one +func IsDateExpired(date, limit time.Time) bool { + date = date.Local() + limit = limit.Local() + + return date.Before(limit) +} + +// IsDateExpiredFromNow tells if the current date and time are greater than the given one +func IsDateExpiredFromNow(date time.Time) bool { + date = date.Local() + today := Now() + + return date.Before(today) +} diff --git a/util/encode.go b/util/encode.go new file mode 100755 index 0000000..04c9267 --- /dev/null +++ b/util/encode.go @@ -0,0 +1,13 @@ +package util + +import "encoding/base64" + +// Encode encodes data using the base64 package +func Encode(src []byte) []byte { + return []byte(base64.URLEncoding.EncodeToString(src)) +} + +// Decode decodes data using the base64 package +func Decode(src []byte) ([]byte, error) { + return base64.URLEncoding.DecodeString(string(src)) +} diff --git a/util/hash.go b/util/hash.go new file mode 100755 index 0000000..f7fd301 --- /dev/null +++ b/util/hash.go @@ -0,0 +1,40 @@ +package util + +import ( + "golang.org/x/crypto/bcrypt" +) + +// HashString returns a hashed string and an error +func HashString(password string) (string, error) { + key, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return "", err + } + + return string(key), nil +} + +// HashBytes returns a hashed byte array and an error +func HashBytes(password []byte) ([]byte, error) { + key, err := bcrypt.GenerateFromPassword(password, bcrypt.DefaultCost) + if err != nil { + return nil, err + } + + return key, nil +} + +// MatchBytes returns true if the hash matches the password +func MatchBytes(hash, password []byte) bool { + err := bcrypt.CompareHashAndPassword(hash, password) + if err == nil { + return true + } + + return false +} + +// MatchString returns true if the hash matches the password +func MatchString(hash, password string) bool { + return MatchBytes([]byte(hash), []byte(password)) +} diff --git a/util/json.go b/util/json.go new file mode 100755 index 0000000..46ad841 --- /dev/null +++ b/util/json.go @@ -0,0 +1,19 @@ +package util + +import "encoding/json" + +// Constants used for JSON serializations +const ( + jsonPrefix = "" + jsonIndent = " " +) + +// SerializeJSON creates the JSON representation of a model +func SerializeJSON(target interface{}) ([]byte, error) { + return json.MarshalIndent(target, jsonPrefix, jsonIndent) +} + +// DeserializeJSON deserializes a JSON representation into a model +func DeserializeJSON(jsonData []byte, target interface{}) error { + return json.Unmarshal(jsonData, target) +} diff --git a/util/uuid.go b/util/uuid.go new file mode 100755 index 0000000..a7586f4 --- /dev/null +++ b/util/uuid.go @@ -0,0 +1,49 @@ +package util + +import ( + "crypto/rand" + "encoding/hex" + "fmt" + "regexp" +) + +const hexPattern = "^(urn\\:uuid\\:)?\\{?([a-z0-9]{8})-([a-z0-9]{4})-" + + "([1-5][a-z0-9]{3})-([a-z0-9]{4})-([a-z0-9]{12})\\}?$" + +var re = regexp.MustCompile(hexPattern) + +// A UUID representation compliant with specification in +// RFC 4122 document. +type UUID [16]byte + +// IsValidUUID tells if a given UUID string is valid +func IsValidUUID(s string) bool { + md := re.FindStringSubmatch(s) + if md == nil { + return false + } + + hash := md[2] + md[3] + md[4] + md[5] + md[6] + + _, err := hex.DecodeString(hash) + if err != nil { + return false + } + + return true +} + +// GenerateUUID creates a new UUID string +func GenerateUUID() (string, error) { + u := new(UUID) + + // Set all bits to randomly (or pseudo-randomly) chosen values. + _, err := rand.Read(u[:]) + if err != nil { + return "", err + } + + u[8] = (u[8] | 0x40) & 0x7F + + return fmt.Sprintf("%x-%x-%x-%x-%x", u[0:4], u[4:6], u[6:8], u[8:10], u[10:]), nil +} diff --git a/wercker.yml b/wercker.yml old mode 100644 new mode 100755 index cf6cc96..f00658f --- a/wercker.yml +++ b/wercker.yml @@ -1,4 +1,4 @@ -box: wercker/golang@1.3.2 +box: wercker/golang@1.4.0 # Build definition # Mongodb service @@ -32,4 +32,4 @@ build: - script: name: go test code: | - go test ./... \ No newline at end of file + go test -v ./... \ No newline at end of file