initial commit 2

This commit is contained in:
Senad Uka
2018-04-25 13:16:36 +02:00
parent c1520d169c
commit 99c10b75fb
167 changed files with 25057 additions and 0 deletions

153
infra/auth/auth.go Normal file
View File

@@ -0,0 +1,153 @@
package auth
import (
"crypto/rsa"
"fmt"
"io/ioutil"
"time"
"github.com/pquerna/ffjson/ffjson"
"strings"
"bitbucket.org/nemt/nemt-portal-api/application/viewmodel"
"bitbucket.org/nemt/nemt-portal-api/infra/config"
"bitbucket.org/nemt/nemt-portal-api/infra/errors"
jwt "github.com/dgrijalva/jwt-go"
"github.com/labstack/echo"
)
const (
// AppKeyHeaderName is the header name for application keys
AppKeyHeaderName = "App"
// AppTokenHeaderName is the header name for application token
AppTokenHeaderName = "Token"
// TokenExpiration is the auth token default expiration time
TokenExpiration = 24 * time.Hour
)
var (
// TokenSigningMethod is the auth token signing algorithm
TokenSigningMethod = jwt.SigningMethodRS256
)
// GetAppKeyFromContext returns the application key from request header
func GetAppKeyFromContext(ctx echo.Context) (appKey string, err error) {
appKey = ctx.Request().Header.Get(AppKeyHeaderName)
if strings.TrimSpace(appKey) == "" {
return "", errors.NewNotAuthorizedError("Application key not found")
}
return appKey, nil
}
// ValidateAppKey validates the presence and validity of App key
func ValidateAppKey(ctx echo.Context, cfg *config.Config) (err error) {
appKey, err := GetAppKeyFromContext(ctx)
if err != nil {
return errors.Wrap(err)
}
if appKey != cfg.HTTP.Auth.AppKey {
return errors.NewNotAuthorizedError("Invalid API Key")
}
return nil
}
// GetCertPrivateKey returns the private key for the token authentication certificate
func GetCertPrivateKey(path string) (pk *rsa.PrivateKey, err error) {
data, err := ioutil.ReadFile(path)
if err != nil {
return nil, errors.Wrap(err)
}
pk, err = jwt.ParseRSAPrivateKeyFromPEM(data)
if err != nil {
return nil, errors.Wrap(err)
}
return pk, nil
}
// GetCertPublicKey returns the public key for the token authentication certificate
func GetCertPublicKey(path string) (pk *rsa.PublicKey, err error) {
privateKey, err := GetCertPrivateKey(path)
if err != nil {
return nil, errors.Wrap(err)
}
return &privateKey.PublicKey, nil
}
// GenerateToken creates a token based on certificate
func GenerateToken(cfg *config.Config, data interface{}) (tokenString string, err error) {
key, _ := GetCertPrivateKey(cfg.HTTP.Auth.CertificatePath)
claims := jwt.MapClaims{
"iat": time.Now().Unix(),
"exp": time.Now().Add(TokenExpiration).Unix(),
"iss": "BDC",
"sub": "NEMT",
"data": data,
}
token := jwt.NewWithClaims(TokenSigningMethod, claims)
tokenString, err = token.SignedString(key)
if err != nil {
return tokenString, errors.Wrap(err)
}
return tokenString, nil
}
func GetTokenDetail(ctx echo.Context, cfg *config.Config) (interface{}, error) {
key, _ := GetCertPublicKey(cfg.HTTP.Auth.CertificatePath)
tokenString := ctx.Request().Header.Get("Token")
tokenString = strings.Replace(tokenString, "Bearer ", "", -1)
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
}
return key, nil
})
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
return claims["data"], nil
} else {
return nil, err
}
}
func GetUserDetail(ctx echo.Context, cfg *config.Config) (viewmodel.User, error) {
key, _ := GetCertPublicKey(cfg.HTTP.Auth.CertificatePath)
tokenString := ctx.Request().Header.Get("Token")
tokenString = strings.Replace(tokenString, "Bearer ", "", -1)
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
}
return key, nil
})
if err != nil {
return viewmodel.User{}, err
}
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
byteData, _ := ffjson.Marshal(claims["data"])
user := viewmodel.User{}
err = ffjson.Unmarshal(byteData, &user)
if err != nil {
return viewmodel.User{}, err
} else {
return user, nil
}
} else {
return viewmodel.User{}, err
}
}

109
infra/aws/aws.go Normal file
View File

@@ -0,0 +1,109 @@
package aws
import (
"bytes"
"net/http"
"path"
"bitbucket.org/nemt/nemt-portal-api/infra/config"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3"
"github.com/aws/aws-sdk-go/service/s3/s3manager"
"github.com/aws/aws-sdk-go/service/ssm"
)
//AWSUtil return the methods to interact with AWS Services
type AWSUtil struct {
configApp *config.Config
awsConfig *aws.Config
s3Service *s3.S3
ssmService *ssm.SSM
awsSession *session.Session
}
//New return an instance of AWSUtil
func New(configApp *config.Config) *AWSUtil {
cfg := aws.NewConfig().WithRegion("sa-east-1")
cfg.DisableRestProtocolURICleaning = aws.Bool(true)
return &AWSUtil{
configApp: configApp,
awsConfig: cfg,
s3Service: s3.New(session.New(), cfg),
ssmService: ssm.New(session.New(), cfg),
awsSession: session.New(cfg),
}
}
//DownloadFromS3Bucket will return the file on the S3 Bucket
func (a AWSUtil) DownloadFromS3Bucket(filePath string) ([]byte, error) {
downloader := s3manager.NewDownloader(a.awsSession)
b := &aws.WriteAtBuffer{}
_, err := downloader.Download(b, &s3.GetObjectInput{
Bucket: aws.String(a.configApp.Aws.S3Bucket),
Key: aws.String(filePath),
})
if err != nil {
return []byte{}, err
} else {
return b.Bytes(), nil
}
}
//UploadToS3Bucket will upload a file to the S3 Bucket
func (a AWSUtil) UploadToS3Bucket(filePath string, fileName string, buff []byte) error {
fileBytes := bytes.NewReader(buff)
fileType := http.DetectContentType(buff)
fullPath := path.Join(filePath, fileName)
params := &s3.PutObjectInput{
Bucket: aws.String(a.configApp.Aws.S3Bucket),
Key: aws.String(fullPath),
Body: fileBytes,
ContentLength: aws.Int64(int64(len(buff))),
ContentType: aws.String(fileType),
}
_, err := a.s3Service.PutObject(params)
if err != nil {
return err
}
return nil
}
//SsmPutParameter put parameter to SSM
func (a AWSUtil) SsmPutParameter(parameterName string, parameterValue string) error {
params := &ssm.PutParameterInput{
Name: aws.String(parameterName),
Type: aws.String("SecureString"),
Value: aws.String(parameterValue),
}
_, err := a.ssmService.PutParameter(params)
if err != nil {
return err
}
return nil
}
//SsmGetParameter get parameter from SSM
func (a AWSUtil) SsmGetParameter(parameterName string, withDecryption bool) (string, error) {
params := &ssm.GetParameterInput{
Name: aws.String(parameterName),
WithDecryption: aws.Bool(withDecryption),
}
output, err := a.ssmService.GetParameter(params)
if err != nil {
return "", err
}
return (*output.Parameter.Value), nil
}

144
infra/cache/cache.go vendored Normal file
View File

@@ -0,0 +1,144 @@
package cache
import (
"encoding/json"
"fmt"
redis "gopkg.in/redis.v5"
"time"
"sync"
"bitbucket.org/nemt/nemt-portal-api/domain"
"bitbucket.org/nemt/nemt-portal-api/domain/contract"
"bitbucket.org/nemt/nemt-portal-api/infra/config"
"bitbucket.org/nemt/nemt-portal-api/infra/errors"
)
var (
instance *RedisCache
once sync.Once
)
// RedisCache implements the CacheManager interface
type RedisCache struct {
cfg *config.Config
redis *redis.Client
}
// Instance returns a CacheManager instance
func Instance(cfg *config.Config) contract.CacheManager {
once.Do(func() {
client := redis.NewFailoverClient(&redis.FailoverOptions{
MasterName: "master01",
SentinelAddrs: []string{fmt.Sprintf("%s:%v", cfg.Cache.Server, cfg.Cache.Port)},
Password: cfg.Cache.Pass,
DB: cfg.Cache.DB,
})
instance = &RedisCache{cfg, client}
})
return instance
}
func (r *RedisCache) buildKey(key string) string {
return fmt.Sprintf("%s-%s", r.cfg.Cache.Prefix, key)
}
// GetItem returns an Item from cache
func (r *RedisCache) GetItem(key string) (data []byte, err error) {
val, err := r.redis.Get(r.buildKey(key)).Bytes()
if err == redis.Nil {
return val, domain.ErrCacheMiss
} else if err != nil {
return val, errors.Wrap(err)
}
return val, nil
}
// SetItem sets an item in cache
func (r *RedisCache) SetItem(key string, data []byte) error {
err := r.redis.Set(r.buildKey(key), data, r.cfg.Cache.DefaultExpiration).Err()
if err != nil {
return errors.Wrap(err)
}
return nil
}
// GetString returns an string from cache
func (r *RedisCache) GetString(key string) (data string, err error) {
val, err := r.GetItem(key)
if err == domain.ErrCacheMiss {
return data, domain.ErrCacheMiss
} else if err != nil {
return data, errors.Wrap(err)
}
return string(val), nil
}
// SetString sets an item in cache
func (r *RedisCache) SetString(key string, data string) error {
err := r.SetItem(key, []byte(data))
if err != nil {
return errors.Wrap(err)
}
return nil
}
// GetStruct returns an struct from cache
func (r *RedisCache) GetStruct(key string, data interface{}) (err error) {
val, err := r.GetItem(key)
if err == domain.ErrCacheMiss {
return domain.ErrCacheMiss
} else if err != nil {
return errors.Wrap(err)
}
err = json.Unmarshal(val, &data)
if err != nil {
return errors.Wrap(err)
}
return nil
}
// SetStruct sets an item in cache
func (r *RedisCache) SetStruct(key string, data interface{}) error {
dataString, err := json.Marshal(data)
if err != nil {
return errors.Wrap(err)
}
err = r.SetItem(key, dataString)
if err != nil {
return errors.Wrap(err)
}
return nil
}
// GetExpiration returns the expiration time for a key
func (r *RedisCache) GetExpiration(key string) (expiration time.Duration, err error) {
expiration, err = r.redis.TTL(r.buildKey(key)).Result()
if err != nil {
return expiration, errors.Wrap(err)
}
return expiration, nil
}
// SetExpiration sets the expiration time for a key
func (r *RedisCache) SetExpiration(key string, expiration time.Duration) (err error) {
err = r.redis.Expire(r.buildKey(key), expiration).Err()
if err != nil {
return errors.Wrap(err)
}
return nil
}

249
infra/config/config.go Normal file
View File

@@ -0,0 +1,249 @@
package config
import (
"strings"
"os"
"time"
"github.com/spf13/viper"
)
// Config represents the configuration values for the whole service.
type Config struct {
App AppConfig
DB DBConfig
HTTP HTTPConfig
Log LogConfig
Aws AwsConfig
Cache CacheConfig
Twilio TwilioConfig
Lyft LyftConfig
LyftProd LyftProdConfig
BXE BXEConfig
Blue365 Blue365Config
Email EmailConfig
GoogleShortener GoogleShortenerConfig
}
// AppConfig represents the configuration values about the application.
type AppConfig struct {
Name string
Debug bool
Docs DocsConfig
}
// TwilioConfig represents the configuration values about the twilio.
type TwilioConfig struct {
Account string
Token string
Sender string
TwiMLSID string
}
// LyftConfig represents the configuration values about the lyft.
type LyftConfig struct {
Client string
Secret string
RefreshToken string
}
type LyftProdConfig struct {
Lyft LyftConfig
UserUUID string
}
// BXEConfig represents the configuration values about the BXE.
type BXEConfig struct {
URL string
APIKey string
Secret string
}
// Blue365Config represents the configuration values about the Blue 365.
type Blue365Config struct {
URL string
APIKey string
Secret string
}
// DBConfig represents the configuration values about the DB.
type DBConfig struct {
User string
Pass string
Name string
Host string
Port int
MaxLifeInMinutes int
MaxIdleConns int
MaxOpenConns int
}
// HTTPConfig represents the configuration values about the HTTP server.
type HTTPConfig struct {
Port int
Prefix string
Auth HTTPAuthConfig
}
// HTTPAuthConfig represents the configuration values about the HTTP authentication (JWT and CORS).
type HTTPAuthConfig struct {
AppKey string
CertificatePath string
FrontendURLs []string
}
// LogConfig represents the configuration values about the logging config.
type LogConfig struct {
LogToFile bool
Path string
}
// AwsConfig represents the configuration values about the aws config.
type AwsConfig struct {
S3Bucket string
}
// DocsConfig represents the configuration values about the documentation config.
type DocsConfig struct {
YAMLPath string
SwaggerPath string
}
// CacheConfig represents the configuration values about the documentation config.
type CacheConfig struct {
Server string
Port int
DB int
Pass string
Prefix string
DefaultExpiration time.Duration
}
// CacheConfig represents the configuration values about the documentation config.
type EmailConfig struct {
Server string
Port int
User string
Pass string
Sender string
}
type GoogleShortenerConfig struct {
APIKey string
ClientID string
SecretKey string
}
// Read returns the configuration values,
// based on the configuration files and environment variables.
func Read() (*Config, error) {
setup()
err := viper.ReadInConfig()
if err != nil {
if err == os.ErrNotExist {
viper.SetConfigName("config.local")
err = viper.ReadInConfig()
}
if err != nil {
return nil, err
}
}
return &Config{
App: AppConfig{
Name: viper.GetString("app.name"),
Debug: viper.GetBool("app.debug"),
Docs: DocsConfig{
YAMLPath: viper.GetString("app.docs.yaml-path"),
SwaggerPath: viper.GetString("app.docs.swagger-path"),
},
},
DB: DBConfig{
User: viper.GetString("db.user"),
Pass: viper.GetString("db.pass"),
Name: viper.GetString("db.name"),
Host: viper.GetString("db.host"),
Port: viper.GetInt("db.port"),
MaxLifeInMinutes: viper.GetInt("db.max-life-minutes"),
MaxIdleConns: viper.GetInt("db.max-idle-conns"),
MaxOpenConns: viper.GetInt("db.max-open-conns"),
},
HTTP: HTTPConfig{
Port: viper.GetInt("http.port"),
Prefix: viper.GetString("http.prefix"),
Auth: HTTPAuthConfig{
AppKey: viper.GetString("http.auth.app-key"),
CertificatePath: viper.GetString("http.auth.certificate-path"),
FrontendURLs: viper.GetStringSlice("http.auth.frontend-urls"),
},
},
Log: LogConfig{
LogToFile: viper.GetBool("log.log-to-file"),
Path: viper.GetString("log.path"),
},
Aws: AwsConfig{
S3Bucket: viper.GetString("aws.s3-bucket"),
},
Cache: CacheConfig{
Server: viper.GetString("cache.server"),
Port: viper.GetInt("cache.port"),
DB: viper.GetInt("cache.db"),
Pass: viper.GetString("cache.pass"),
Prefix: viper.GetString("cache.prefix"),
DefaultExpiration: viper.GetDuration("cache.default-expiration"),
},
Lyft: LyftConfig{
Client: viper.GetString("lyft.key"),
Secret: viper.GetString("lyft.secret"),
RefreshToken: viper.GetString("lyft.token"),
},
LyftProd: LyftProdConfig{
Lyft: LyftConfig{
Client: viper.GetString("lyft-prod.key"),
Secret: viper.GetString("lyft-prod.secret"),
RefreshToken: viper.GetString("lyft-prod.token"),
},
UserUUID: viper.GetString("lyft-prod.user-uuid"),
},
Twilio: TwilioConfig{
Account: viper.GetString("twilio.account"),
Token: viper.GetString("twilio.token"),
Sender: viper.GetString("twilio.sender"),
TwiMLSID: viper.GetString("twilio.twiml-sid"),
},
BXE: BXEConfig{
URL: viper.GetString("bxe.url"),
APIKey: viper.GetString("bxe.key"),
Secret: viper.GetString("bxe.secret"),
},
Blue365: Blue365Config{
URL: viper.GetString("blue365.url"),
APIKey: viper.GetString("blue365.key"),
Secret: viper.GetString("blue365.secret"),
},
Email: EmailConfig{
Server: viper.GetString("email.server"),
Port: viper.GetInt("email.port"),
User: viper.GetString("email.user"),
Pass: viper.GetString("email.pass"),
Sender: viper.GetString("email.sender"),
},
GoogleShortener: GoogleShortenerConfig{
APIKey: viper.GetString("google-shortener.api-key"),
ClientID: viper.GetString("google-shortener.client-id"),
SecretKey: viper.GetString("google-shortener.secret-key"),
},
}, nil
}
func setup() {
viper.SetEnvPrefix("api")
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
viper.AutomaticEnv()
viper.SetConfigName("config")
viper.AddConfigPath(".")
}

177
infra/errors/errors.go Normal file
View File

@@ -0,0 +1,177 @@
package errors
import (
"fmt"
"net/http"
"runtime"
"strings"
)
type errorWrapper interface {
Error() string
GetOriginalError() *error
}
// WrappedError holds an error wrapped with a context message
type WrappedError struct {
originalError *error
path string
messages []string
}
func (err WrappedError) Error() string {
if len(err.messages) > 0 {
retVal := fmt.Sprintf("%s: ", err.path)
for _, message := range err.messages {
retVal += message + "; "
}
return fmt.Sprintf("%s => %v", retVal, *err.originalError)
}
return fmt.Sprintf("%s => %v", err.path, *err.originalError)
}
// GetOriginalError returns the original error
func (err WrappedError) GetOriginalError() *error {
if err.originalError != nil {
if originalError, ok := (*err.originalError).(errorWrapper); ok {
return originalError.GetOriginalError()
}
}
return err.originalError
}
// Wrap wraps an error with a context message
func Wrap(err error, messages ...string) error {
if err != nil {
// get caller function path
pc := make([]uintptr, 10)
runtime.Callers(2, pc)
funcRef := runtime.FuncForPC(pc[0])
pathArr := strings.Split(funcRef.Name(), "/")
path := pathArr[len(pathArr)-1]
return &WrappedError{
originalError: &err,
path: path,
messages: messages,
}
}
return nil
}
// NewValidationError returns a ValidationError instance with the provided parameters
func NewValidationError(field string, message string) *ValidationError {
return &ValidationError{
Field: field,
Message: message,
}
}
// NewNullArgumentError returns a preformatted error for null arguments
func NewNullArgumentError(argumentName string) *NullArgumentError {
return &NullArgumentError{argumentName}
}
// NewApplicationError returns a ApplicationError instance
func NewApplicationError(path string, message string) *ApplicationError {
return &ApplicationError{
Message: message,
Path: path,
}
}
// NewNotAuthorizedError returns a NotAuthorizedError instance
func NewNotAuthorizedError(message string) *NotAuthorizedError {
return &NotAuthorizedError{
Message: message,
}
}
// NewHTTPError returns a HTTPError instance
func NewHTTPError(httpStatus int, message string) *HTTPError {
return &HTTPError{
Status: httpStatus,
Message: message,
}
}
// NewNotFoundError returns a Not Found HTTPError instance
func NewNotFoundError() *HTTPError {
return NewHTTPError(http.StatusNotFound, "Not Found")
}
// HTTPError represents a generic HTTP error response
type HTTPError struct {
Status int
Message string
}
func (e *HTTPError) Error() string {
return fmt.Sprintf("%v - %s", e.Status, e.Message)
}
// NullArgumentError represents a error that is used to sinalize that a provided argument is null
type NullArgumentError struct {
ArgumentName string
}
func (e *NullArgumentError) Error() string {
return fmt.Sprintf("Parameter %s can't be null", e.ArgumentName)
}
// ValidationError represents an input validation error
type ValidationError struct {
Field string `json:"field_name,omitempty"`
Message string `json:"message,omitempty"`
Errors []ValidationError `json:"errors,omitempty"`
}
func (e *ValidationError) Error() string {
var errorList []string
for _, err := range e.Errors {
errorList = append(errorList, err.Error())
}
output := e.Message
if len(errorList) > 0 {
output += fmt.Sprintf("\n - %v", strings.Join(errorList, ";\n - "))
}
return fmt.Sprintf("%v: %v", e.Field, output)
}
// AddError adds a new validation error to the chain
func (e *ValidationError) AddError(field string, message string) {
e.Errors = append(e.Errors, ValidationError{
Field: field,
Message: message,
})
}
// ApplicationError represents a common applicatino error structure
type ApplicationError struct {
Message string
Path string
}
func (e *ApplicationError) Error() string {
return e.Message
}
// NotAuthorizedError represents an access restriction error structure
type NotAuthorizedError struct {
Message string
}
func (e *NotAuthorizedError) Error() string {
return e.Message
}

53
infra/logger/logger.go Normal file
View File

@@ -0,0 +1,53 @@
package logger
import (
"fmt"
"os"
"io"
"bitbucket.org/nemt/nemt-portal-api/infra/config"
"github.com/Sirupsen/logrus"
)
// Logger is the default application logger compatible with the echo.Logger interface
type Logger struct {
cfg *config.Config
*logrus.Entry
}
// InfoWriter returns the io.Writer for info level
func (l *Logger) InfoWriter() io.Writer {
return l.WriterLevel(logrus.InfoLevel)
}
// ErrorWriter returns the io.Writer for error level
func (l *Logger) ErrorWriter() io.Writer {
return l.WriterLevel(logrus.ErrorLevel)
}
// New returns a new Logger instance
func New(cfg *config.Config) (*Logger, error) {
if cfg.Log.LogToFile {
file, err := os.Create(cfg.Log.Path)
if err != nil {
fmt.Printf("Error to create log file for library: %s\n", err.Error())
panic(err)
}
logrus.SetOutput(file)
}
logrus.SetFormatter(&logrus.JSONFormatter{})
if cfg.App.Debug {
logrus.SetLevel(logrus.DebugLevel)
} else {
logrus.SetLevel(logrus.InfoLevel)
}
entry := logrus.WithFields(logrus.Fields{
"app": cfg.App.Name,
})
return &Logger{cfg, entry}, nil
}