Skip to content
Snippets Groups Projects
Select Git revision
  • bb9b08a1e575ba35c1c73d2fe254969d6d6b1af6
  • dev default protected
  • prod protected
  • 1.0.58
  • 1.0.57
  • 1.0.52
  • 1.0.56
  • 1.0.51
  • 1.0.50
  • 1.0.33
  • 1.0.32
  • 1.0.31
  • 1.0.30
  • 1.0.29
  • 1.0.28
  • 1.0.27
  • 1.0.26
  • 1.0.25
  • 1.0.24
  • 1.0.23
  • 1.0.22
  • 1.0.21
  • 1.0.20
23 results

OrderCreateWebhook.php

Blame
  • lambda.go 9.55 KiB
    package api
    
    import (
    	"context"
    	"database/sql"
    	"encoding/json"
    	"fmt"
    	"math/rand"
    	"net/http"
    	"reflect"
    	"time"
    
    	"github.com/aws/aws-lambda-go/events"
    	"github.com/aws/aws-lambda-go/lambdacontext"
    	"gitlab.com/uafrica/go-utils/errors"
    	"gitlab.com/uafrica/go-utils/logger"
    )
    
    func (api Api) NewContext(baseCtx context.Context, requestID string, request events.APIGatewayProxyRequest) (Context, error) {
    	serviceContext, err := api.Service.NewContext(baseCtx, requestID, nil)
    	if err != nil {
    		return nil, err
    	}
    
    	return &apiContext{
    		Context: serviceContext,
    		request: request,
    	}, nil
    }
    
    //this is native handler for lambda passed into lambda.Start()
    //to run locally, this is called from app.ServeHTTP()
    func (api Api) Handler(baseCtx context.Context, apiGatewayProxyReq events.APIGatewayProxyRequest) (res events.APIGatewayProxyResponse, err error) {
    	res = events.APIGatewayProxyResponse{
    		StatusCode: http.StatusInternalServerError,
    		Body:       "undefined response",
    		Headers:    map[string]string{},
    	}
    
    	// Replace the proxy resource with the path, has some edge cases but works for our current API implementation
    	// Edge case being that if have path params then specify those routes explicitly
    	if apiGatewayProxyReq.Resource == "/{proxy+}" {
    		apiGatewayProxyReq.Resource = apiGatewayProxyReq.Path
    	}
    
    	//get request-id from HTTP headers (used when making internal service calls)
    	//if not defined in header, get the AWS request id from the AWS context
    	requestID, ok := apiGatewayProxyReq.Headers[api.requestIDHeaderKey]
    	if !ok || requestID == "" {
    		if lambdaContext, ok := lambdacontext.FromContext(baseCtx); ok && lambdaContext != nil {
    			requestID = lambdaContext.AwsRequestID
    		}
    	}
    
    	//service context invoke the starters and could fail, e.g. if cannot connect to db
    	ctx, err := api.NewContext(baseCtx, requestID, apiGatewayProxyReq)
    	if err != nil {
    		return res, err
    	}
    
    	//report handler crashes
    	if api.crashReporter != nil {
    		defer api.crashReporter.Catch(ctx)
    	}
    
    	defer func() {
    		//set CORS headers on every response
    		if api.cors != nil {
    			for n, v := range api.cors.CORS() {
    				res.Headers[n] = v
    			}
    		}
    	}()
    
    	defer func() {
    		ctx.LogAPIRequestAndResponse(res, err)
    		if err != nil {
    			ctx.Errorf("failed: %+v", err)
    
    			//try to retrieve HTTP code from error
    			if withCause, ok := err.(errors.ErrorWithCause); ok && withCause.Code() != 0 {
    				res.StatusCode = withCause.Code()
    				err = withCause.Cause() //drop the http layers + up
    			} else {
    				//no HTTP code indicate in err,
    				//see if there are SQL errors that could indicate code
    				if code, ok := StatusCodeFromSQLError(err); ok {
    					res.StatusCode = code
    				}
    			}
    
    			errorMessage := fmt.Sprintf("%c", err)
    			jsonError, _ := json.Marshal(map[string]interface{}{"message": errorMessage})
    			res.Headers["Content-Type"] = "application/json"
    			res.Body = string(jsonError)
    			err = nil //never pass error back to lambda or http server
    		}
    		if api.requestIDHeaderKey != "" {
    			res.Headers[api.requestIDHeaderKey] = ctx.RequestID()
    		}
    		if err := api.Service.WriteValues(ctx.StartTime(), time.Now(), ctx.RequestID(), map[string]interface{}{
    			"direction":  "incoming",
    			"type":       "api",
    			"request_id": ctx.RequestID(),
    			"request":    ctx.Request(),
    			"response":   res},
    		); err != nil {
    			ctx.Errorf("failed to audit: %+v", err)
    		}
    	}()
    
    	//Early return OPTIONS call
    	if apiGatewayProxyReq.HTTPMethod == "OPTIONS" {
    		res.StatusCode = http.StatusNoContent
    		err = nil
    		return
    	}
    
    	rand.Seed(time.Now().Unix())
    
    	for checkName, check := range api.checks {
    		var checkData interface{}
    		checkData, err = check.Check(ctx)
    		if err != nil {
    			err = errors.Wrapf(err, "%s", checkName)
    			return
    		}
    		if err = ctx.Set(checkName, checkData); err != nil {
    			err = errors.Wrapf(err, "failed to set check(%s) data=(%T)%+v", checkName, checkData, checkData)
    			return
    		}
    	}
    
    	//LEGACY: delete this as soon as all handlers accepts context
    	//this does not support concurrent execution!
    	CurrentRequestID = &apiGatewayProxyReq.RequestContext.RequestID
    
    	ctx.Debugf("HTTP %s %s ...\n", apiGatewayProxyReq.HTTPMethod, apiGatewayProxyReq.Resource)
    	ctx.WithFields(map[string]interface{}{
    		"http_method":                ctx.Request().HTTPMethod,
    		"path":                       ctx.Request().Path,
    		"api_gateway_request_id":     ctx.Request().RequestContext.RequestID,
    		"user_cognito_auth_provider": ctx.Request().RequestContext.Identity.CognitoAuthenticationProvider,
    		"user_arn":                   ctx.Request().RequestContext.Identity.UserArn,
    	}).Infof("Start API Handler")
    
    	//TODO:
    	// // Get claims and check the status of the user
    	// ctx.Claims, err = api.RetrieveClaims(&apiGatewayProxyReq)
    	// if err != nil {
    	// 	return events.APIGatewayProxyResponse{
    	// 		StatusCode: http.StatusBadRequest,
    	// 		Body:       fmt.Sprintf("%v\n", err),
    	// 		Headers:    utils.CorsHeaders(),
    	// 	}, nil
    	// }
    
    	// if ctx.Claims.UserID != nil {
    	// 	userStatusResponse := checkUserStatus(ctx.Claims)
    	// 	if userStatusResponse != nil {
    	// 		return *userStatusResponse, nil
    	// 	}
    	// }
    
    	// permissionString := fmt.Sprintf("API_%s%s:%s", os.Getenv("MICRO_SERVICE_API_BASE_PATH"), apiGatewayProxyReq.Resource, apiGatewayProxyReq.HTTPMethod)
    	// if !permissions.HasPermission(ctx.Claims.Role, permissionString) {
    	// 	response, _ := apierr.ClientError(http.StatusUnauthorized, fmt.Sprintf("You do not have access to the requested resource: %s", permissionString))
    	// 	if ctx.Claims.Role == nil {
    	// 		ctx.Errorf("%d :: %s: %v", *ctx.Claims.RoleID, permissionString, fmt.Errorf("you have no role"))
    	// 	} else if ctx.Claims.RoleID == nil {
    	// 		ctx.Errorf("%s: you have no role ID", permissionString)
    	// 	}
    	// 	return response, nil
    	// }
    
    	//route on method and path
    	resourceHandler, err := api.router.Route(apiGatewayProxyReq.Resource, apiGatewayProxyReq.HTTPMethod)
    	if err != nil {
    		err = errors.Wrapf(err, "invalid route")
    		return
    	}
    
    	if legacyHandlerFunc, ok := resourceHandler.(func(req events.APIGatewayProxyRequest) (response events.APIGatewayProxyResponse, err error)); ok {
    		ctx.Debugf("Calling legacy handler...")
    		return legacyHandlerFunc(apiGatewayProxyReq)
    	}
    
    	handler, ok := resourceHandler.(handler)
    	if !ok {
    		//should not get here if validateAPIEndpoints() is properly checking!
    		err = errors.HTTP(http.StatusInternalServerError, errors.Errorf("invalid handler function %T", resourceHandler), "invalid routing")
    	}
    
    	//new type of handler function
    	//allocate, populate and validate params struct
    	paramsStruct, paramsErr := ctx.GetRequestParams(handler.RequestParamsType)
    	if paramsErr != nil {
    		err = errors.HTTP(http.StatusBadRequest, paramsErr, "invalid parameters")
    		return
    	}
    	//apply claims to params struct - TODO: Removed - see if cannot force to get claims from context always
    	// if err = ctx.Claims.FillOnObject(ctx.request, &paramsStruct); err != nil {
    	// 	err = errors.HTTP(http.StatusInternalServerError, err, "claims failed on parameters")
    	// 	return
    	// }
    	ctx.Debugf("Params: (%T) %+v", paramsStruct, paramsStruct)
    
    	args := []reflect.Value{
    		reflect.ValueOf(ctx),
    		reflect.ValueOf(paramsStruct),
    	}
    
    	var bodyStruct interface{}
    	if handler.RequestBodyType != nil {
    		//allocate, populate and validate request struct
    		bodyStruct, err = ctx.GetRequestBody(handler.RequestBodyType)
    		if err != nil {
    			err = errors.HTTP(http.StatusBadRequest, err, "invalid body")
    			return
    		}
    
    		//apply claims to request struct - TODO: Removed - see if cannot force to get claims from context always
    		// if err = ctx.Claims.FillOnObject(ctx.request, &bodyStruct); err != nil {
    		// 	err = errors.HTTP(http.StatusInternalServerError, err, "claims failed on body")
    		// 	return
    		// }
    
    		ctx.Debugf("Body: (%T) %+v", bodyStruct, bodyStruct)
    		args = append(args, reflect.ValueOf(bodyStruct))
    	}
    
    	//call handler in a func with defer to catch potential crash
    	ctx.Infof("Calling handle %s %s ...", apiGatewayProxyReq.HTTPMethod, apiGatewayProxyReq.Resource)
    	var results []reflect.Value
    	results, err = func() (results []reflect.Value, err error) {
    		defer func() {
    			if crashErr := recover(); crashErr != nil {
    				stack := logger.CallStack()
    				err = errors.Errorf("handler function crashed: %v, with stack: %+v", crashErr, stack)
    				return
    			}
    		}()
    		results = handler.FuncValue.Call(args)
    		return results, nil
    	}()
    	if err != nil {
    		err = errors.Wrapf(err, "handler failed")
    		return
    	}
    
    	//ctx.Debugf("handler -> results: %v", results)
    	//see if handler failed using last result of type error
    	lastResultValue := results[len(results)-1]
    	if !lastResultValue.IsNil() {
    		err = lastResultValue.Interface().(error)
    		if err != nil {
    			return
    		}
    	}
    
    	//handler succeeded, some handler does not have a response data (typically post/put/patch/delete)
    	err = nil
    	switch apiGatewayProxyReq.HTTPMethod {
    	case http.MethodDelete:
    		res.StatusCode = http.StatusNoContent
    	default:
    		res.StatusCode = http.StatusOK
    	}
    
    	if len(results) > 1 {
    		responseStruct := results[0].Interface()
    		ctx.Debugf("Response type: %T", responseStruct)
    
    		var bodyBytes []byte
    		bodyBytes, err = json.Marshal(responseStruct)
    		if err != nil {
    			err = errors.Wrapf(err, "failed to encode response content")
    			return
    		}
    		res.Headers["Content-Type"] = "application/json"
    		res.Body = string(bodyBytes)
    	} else {
    		//no content
    		delete(res.Headers, "Content-Type")
    		res.Body = ""
    	}
    	return
    }
    
    //look for SQL errors in the error stack
    func StatusCodeFromSQLError(err error) (int, bool) {
    	if err == sql.ErrNoRows {
    		return http.StatusNotFound, true
    	}
    
    	if errWithCause, ok := err.(errors.ErrorWithCause); ok {
    		return StatusCodeFromSQLError(errWithCause.Cause())
    	}
    
    	//could not determine known SQL error
    	return 0, false
    }