Template files added
parent
379f02e987
commit
4fd71945e9
|
|
@ -0,0 +1,274 @@
|
|||
package authn51
|
||||
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
"fmt"
|
||||
"time"
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/google/uuid"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/getkin/kin-openapi/openapi3filter"
|
||||
)
|
||||
|
||||
|
||||
type Authn51 struct {
|
||||
Environment string
|
||||
Providers []string
|
||||
AuthnFunc openapi3filter.AuthenticationFunc
|
||||
Bearer struct {
|
||||
Secret string
|
||||
DefaultUser string
|
||||
}
|
||||
Basic struct {
|
||||
DefaultUser string
|
||||
Password string
|
||||
}
|
||||
Apikey struct {
|
||||
ApiValue string
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
type Configuration struct {
|
||||
Environment string `yaml:"environment"`
|
||||
Authn51 struct {
|
||||
Bearer struct {
|
||||
Secret string `yaml:"secret"`
|
||||
DefaultUser string `yaml:"default_user"`
|
||||
} `yaml:"bearer"`
|
||||
Basic struct {
|
||||
DefaultUser string `yaml:"default_user"`
|
||||
Password string `yaml:"password"`
|
||||
} `yaml:"basic"`
|
||||
Apikey struct {
|
||||
ApiValue string `yaml:"api_value"`
|
||||
} `yaml:"api_key"`
|
||||
} `yaml:"authn51"`
|
||||
}
|
||||
|
||||
|
||||
type AuthenticationToken struct {
|
||||
Environment string `yaml:"environment"`
|
||||
Token string `yaml:"token"`
|
||||
Claims struct {
|
||||
UserId string `yaml:"user_id"`
|
||||
Exp string `yaml:"exp"`
|
||||
} `yaml:"claims"`
|
||||
}
|
||||
|
||||
func NewAuthn51(configPath string) (*Authn51, error) {
|
||||
|
||||
var authn51 Authn51
|
||||
var config Configuration
|
||||
|
||||
file, err := os.Open(configPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
d := yaml.NewDecoder(file)
|
||||
|
||||
if err := d.Decode(&config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
authn51.Bearer.DefaultUser = config.Authn51.Bearer.DefaultUser
|
||||
authn51.Bearer.Secret = config.Authn51.Bearer.Secret
|
||||
authn51.AuthnFunc = authn51.AuthenticationFunc
|
||||
if (authn51.Bearer.Secret == "%ACCESS_SECRET%") {
|
||||
authn51.Bearer.Secret = os.Getenv("%ACCESS_SECRET%")
|
||||
}
|
||||
if (authn51.Bearer.Secret == "") {
|
||||
log.Printf("No secret supplied. Generating random value")
|
||||
authn51.Bearer.Secret = uuid.New().String()
|
||||
}
|
||||
|
||||
return &authn51, nil
|
||||
}
|
||||
|
||||
|
||||
func (self *Authn51) CreateToken(userId string) (AuthenticationToken, error) {
|
||||
var err error
|
||||
var authToken AuthenticationToken
|
||||
|
||||
exp := time.Now().Add(time.Minute * 20)
|
||||
|
||||
atClaims := jwt.MapClaims{}
|
||||
atClaims["authorized"] = true
|
||||
atClaims["organization"] = "any"
|
||||
atClaims["user_id"] = userId
|
||||
atClaims["exp"] = exp.Unix()
|
||||
|
||||
at := jwt.NewWithClaims(jwt.SigningMethodHS256, atClaims)
|
||||
token, err := at.SignedString([]byte(self.Bearer.Secret))
|
||||
if err != nil {
|
||||
return authToken, err
|
||||
}
|
||||
|
||||
authToken.Token = token
|
||||
authToken.Claims.UserId = userId
|
||||
authToken.Claims.Exp = exp.String()
|
||||
|
||||
return authToken, nil
|
||||
}
|
||||
|
||||
|
||||
|
||||
func (self *Authn51) extractToken(r *http.Request) string {
|
||||
bearToken := r.Header.Get("Authorization")
|
||||
strArr := strings.Split(bearToken, " ")
|
||||
if len(strArr) == 2 {
|
||||
return strArr[1]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (self *Authn51) verifyToken(r *http.Request) (*jwt.Token, error) {
|
||||
tokenString := self.extractToken(r)
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
//Make sure that the token method conform to "SigningMethodHMAC"
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return []byte(self.Bearer.Secret), nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func (self *Authn51) tokenValid(r *http.Request) error {
|
||||
token, err := self.verifyToken(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, ok := token.Claims.(jwt.Claims); !ok && !token.Valid {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type AccessDetails struct {
|
||||
Organization string
|
||||
UserId string
|
||||
}
|
||||
|
||||
func (self *Authn51) ExtractTokenMetadata(r *http.Request) (*AccessDetails, error) {
|
||||
token, err := self.verifyToken(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if ok && token.Valid {
|
||||
organization, ok := claims["organization"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Extract error with missing '%s'", "organization")
|
||||
}
|
||||
userId, ok := claims["user_id"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Extract error with missing '%s'", "user_id")
|
||||
}
|
||||
return &AccessDetails{
|
||||
Organization: organization,
|
||||
UserId: userId,
|
||||
}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("Token extract error: %s", "unknown")
|
||||
}
|
||||
|
||||
|
||||
func (self *Authn51) AuthenticationFunc(ctx context.Context, ai *openapi3filter.AuthenticationInput) error {
|
||||
|
||||
schemeN := ai.SecuritySchemeName
|
||||
if (schemeN == "") {
|
||||
log.Print("Scheme name is blank")
|
||||
}
|
||||
|
||||
req := ai.RequestValidationInput.Request
|
||||
|
||||
if (ai.SecurityScheme.Scheme == "basic") {
|
||||
userName, password, ok := req.BasicAuth()
|
||||
if (ok){
|
||||
if (userName == "joe" && password == "secret") {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
err := errors.New("Basic authentication failed!")
|
||||
msg := fmt.Errorf("security requirement '%q' failed", schemeN)
|
||||
return &echo.HTTPError{
|
||||
Code: http.StatusUnauthorized,
|
||||
Message: msg,
|
||||
Internal: err,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (ai.SecurityScheme.Scheme == "bearer") {
|
||||
|
||||
ad, err := self.ExtractTokenMetadata(req)
|
||||
if (err != nil || ad == nil) {
|
||||
|
||||
log.Print("Bearer authentication failed extraction!")
|
||||
err := errors.New("Bearer authentication failed!")
|
||||
msg := fmt.Errorf("security requirement '%q' failed", schemeN)
|
||||
return &echo.HTTPError{
|
||||
Code: http.StatusUnauthorized,
|
||||
Message: msg,
|
||||
Internal: err,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (ad.UserId != "") {
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Print("Bearer authentication failed user id!")
|
||||
err = errors.New("Bearer authentication failed!")
|
||||
msg := fmt.Errorf("security requirement %q failed", schemeN)
|
||||
return &echo.HTTPError{
|
||||
Code: http.StatusUnauthorized,
|
||||
Message: msg,
|
||||
Internal: err,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (ai.SecurityScheme.Type == "apiKey") {
|
||||
auth := req.Header.Get("X-ApiKey")
|
||||
if auth == "secret" {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := errors.New("API key authentication failed!")
|
||||
msg := fmt.Errorf("security requirement '%q' failed", schemeN)
|
||||
return &echo.HTTPError{
|
||||
Code: http.StatusUnauthorized,
|
||||
Message: msg,
|
||||
Internal: err,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
log.Printf("security requirement '%q' failed", schemeN)
|
||||
err := errors.New("Authentication failed!")
|
||||
msg := fmt.Errorf("security requirement '%q' failed", schemeN)
|
||||
return &echo.HTTPError{
|
||||
Code: http.StatusUnauthorized,
|
||||
Message: msg,
|
||||
Internal: err,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,185 @@
|
|||
package api
|
||||
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
|
||||
|
||||
type SoftwareConfiguration struct {
|
||||
configFile string
|
||||
// Execution environment
|
||||
// If not psecified or recognised then assume product
|
||||
// for safey. Vales are:
|
||||
// - production
|
||||
// - staging
|
||||
// - testing
|
||||
// - development
|
||||
Environment string `yaml:"environment"`
|
||||
Server struct {
|
||||
// IP Address to bind the HTTP Server to
|
||||
Host string `yaml:"host"`
|
||||
// TCP Port to bind the HTTP Server to
|
||||
Port int `yaml:"port"`
|
||||
// Service base path
|
||||
BasePath string `yaml:"base_path"`
|
||||
// Timeout vaues
|
||||
Timeout struct {
|
||||
// Server is the general server timeout to use
|
||||
// for graceful shutdowns
|
||||
Server time.Duration `yaml:"server"`
|
||||
|
||||
// Write is the amount of time to wait until an HTTP server
|
||||
// write opperation is cancelled
|
||||
Write time.Duration `yaml:"write"`
|
||||
|
||||
// Read is the amount of time to wait until an HTTP server
|
||||
// read operation is cancelled
|
||||
Read time.Duration `yaml:"read"`
|
||||
|
||||
// Read is the amount of time to wait
|
||||
// until an IDLE HTTP session is closed
|
||||
Idle time.Duration `yaml:"idle"`
|
||||
} `yaml:"timeout"`
|
||||
} `yaml:"server"`
|
||||
Folders struct {
|
||||
// Data folder
|
||||
Data string `yaml:"data"`
|
||||
// Static HTLM, CSS folder
|
||||
Static string `yaml:"static"`
|
||||
// Configuration folder
|
||||
Config string `yaml:"config"`
|
||||
} `yaml:"folders"`
|
||||
DataSource struct {
|
||||
// Load products on start up
|
||||
Load bool `yaml:"load"`
|
||||
// Overwrte existing product data
|
||||
Overwrite bool `yaml:"overwrite"`
|
||||
} `yaml:"dataSource"`
|
||||
Api struct {
|
||||
// Deafult page size for lists
|
||||
PageSize int `yaml:"page_size"`
|
||||
} `yaml:"api"`
|
||||
Hosts struct {
|
||||
// Product protocol, host name and port, e.g.http://localhost:8075
|
||||
Product string `yaml:"product"`
|
||||
// Portfolio, account protocol, host name and port, e.g.http://localhost:8076
|
||||
Protfolio string `yaml:"portfolio"`
|
||||
// Quote protocol, host name and port, e.g.http://localhost:8077
|
||||
Quote string `yaml:"quote"`
|
||||
// Order managemet protocol, host name and port, e.g.http://localhost:8077
|
||||
Order string `yaml:"order"`
|
||||
// Search protocol, host name and port, e.g.http://localhost:8075
|
||||
Search string `yaml:"search"`
|
||||
} `yaml:"hosts"`
|
||||
Support struct {
|
||||
// Message verbose level, 1=minimum message, 5=All messages
|
||||
Level int `yaml:"level"`
|
||||
} `yaml:"support"`
|
||||
}
|
||||
|
||||
func NewSoftwareConfiguration() *SoftwareConfiguration {
|
||||
|
||||
var sc = SoftwareConfiguration{}
|
||||
|
||||
sc.Environment = "production"
|
||||
sc.Server.BasePath = "/"
|
||||
|
||||
sc.DataSource.Load = true
|
||||
sc.DataSource.Overwrite = false
|
||||
|
||||
sc.Folders.Static = "./static"
|
||||
sc.Folders.Data = "./data"
|
||||
sc.Folders.Config = "./config"
|
||||
|
||||
sc.Support.Level= 1
|
||||
|
||||
sc.Api.PageSize = 20
|
||||
|
||||
return &sc
|
||||
}
|
||||
|
||||
func (self SoftwareConfiguration) GetConfigurationFile() string {
|
||||
return self.configFile
|
||||
}
|
||||
|
||||
func LoadConfig(configPath string) (*SoftwareConfiguration, error) {
|
||||
|
||||
config := &SoftwareConfiguration{}
|
||||
|
||||
file, err := os.Open(configPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
d := yaml.NewDecoder(file)
|
||||
|
||||
if err := d.Decode(&config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config.configFile = configPath
|
||||
|
||||
if (config.Environment == "") {
|
||||
config.Environment = "production"
|
||||
}
|
||||
|
||||
// Perform some validation
|
||||
if (config.Environment != "production" &&
|
||||
config.Environment != "staging" &&
|
||||
config.Environment != "testing" &&
|
||||
config.Environment != "development") {
|
||||
log.Printf("Environment value '%s' not valid. Reverting to defauult", config.Environment)
|
||||
config.Environment = "production"
|
||||
}
|
||||
|
||||
if (config.Api.PageSize < 1) {
|
||||
config.Api.PageSize = 20
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func ParseFlags(configPath string) (*SoftwareConfiguration, error) {
|
||||
|
||||
flag.StringVar(&configPath, "config", configPath, "path to config file")
|
||||
var port = flag.Int("port", 0, "Port for HTTP server micro service")
|
||||
var dataFolder = flag.String("data", "", "Data folder")
|
||||
var staticFolder = flag.String("static", "", "Static folder")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
fileStat, err := os.Stat(configPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if fileStat.IsDir() {
|
||||
return nil, fmt.Errorf("'%s' is a directory, not a normal file", configPath)
|
||||
}
|
||||
|
||||
config, err := LoadConfig(configPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if (*port > 0) {
|
||||
config.Server.Port = *port
|
||||
}
|
||||
if (*dataFolder != "") {
|
||||
config.Folders.Data = *dataFolder
|
||||
}
|
||||
if (*staticFolder != "") {
|
||||
config.Folders.Static = *staticFolder
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package errorh
|
||||
|
||||
|
||||
|
||||
// A common error payload returned
|
||||
// when the response code is not 2xx
|
||||
type ErrorModel struct {
|
||||
// Error description, that shuld be less technical
|
||||
// and more user orientated where possible
|
||||
Message string `json:"message,omitempty"`
|
||||
|
||||
// Information on how the error or issue may
|
||||
// be resolved.
|
||||
Resolution string `json:"resolution,omitempty"`
|
||||
|
||||
// Status identifier that can be used to identify the
|
||||
// cause of the error.
|
||||
//
|
||||
// It s not the HTTP status code (eg 4xx ot 5xx)
|
||||
Status string `json:"status,omitempty"`
|
||||
|
||||
// Technical information for the error.
|
||||
//
|
||||
// This must not contain sensitive information
|
||||
Technical string `json:"technical,omitempty"`
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
package errorh
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/labstack/echo/v4"
|
||||
"net/http"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type (
|
||||
httpErrorHandler struct {
|
||||
statusCodes map[error]int
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
func NewHttpErrorHandler() *httpErrorHandler {
|
||||
return &httpErrorHandler{}
|
||||
}
|
||||
|
||||
func (self *httpErrorHandler) getStatusCode(err error) int {
|
||||
for key, value := range self.statusCodes {
|
||||
if errors.Is(err, key) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
return http.StatusInternalServerError
|
||||
}
|
||||
|
||||
func unwrapRecursive(err error) error {
|
||||
var originalErr = err
|
||||
|
||||
for originalErr != nil {
|
||||
var internalErr = errors.Unwrap(originalErr)
|
||||
|
||||
if internalErr == nil {
|
||||
break
|
||||
}
|
||||
|
||||
originalErr = internalErr
|
||||
}
|
||||
|
||||
return originalErr
|
||||
}
|
||||
|
||||
func (self *httpErrorHandler) Handler(err error, c echo.Context) {
|
||||
|
||||
technical := ""
|
||||
he, ok := err.(*echo.HTTPError)
|
||||
if ok {
|
||||
if he.Internal != nil {
|
||||
if herr, ok := he.Internal.(*echo.HTTPError); ok {
|
||||
he = herr
|
||||
}
|
||||
technical = fmt.Sprintf("%v", he.Internal)
|
||||
}
|
||||
} else {
|
||||
he = &echo.HTTPError{
|
||||
Code: self.getStatusCode(err),
|
||||
Message: unwrapRecursive(err).Error(),
|
||||
}
|
||||
}
|
||||
|
||||
code := he.Code
|
||||
stdError := ErrorModel{
|
||||
Message: fmt.Sprintf("%v", he.Message),
|
||||
Resolution: "",
|
||||
Status: fmt.Sprintf("DM00%d", code),
|
||||
Technical: technical,
|
||||
}
|
||||
|
||||
if (code == 401) {
|
||||
stdError.Resolution = "Please provide authentication details. These can be API key, Basic Auth, Bearer Auth."
|
||||
}
|
||||
if (code == 400) {
|
||||
if (stdError.Message == "no matching operation was found") {
|
||||
stdError.Resolution = "Please check the Operation name and resource and correct to a valid name. Reference the Open API specification for valid names"
|
||||
}
|
||||
}
|
||||
|
||||
// Send response
|
||||
if !c.Response().Committed {
|
||||
if c.Request().Method == http.MethodHead {
|
||||
err = c.NoContent(he.Code)
|
||||
} else {
|
||||
err = c.JSON(code, stdError)
|
||||
}
|
||||
if err != nil {
|
||||
c.Echo().Logger.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
|
||||
"{{moduleName}}/api/gen"
|
||||
)
|
||||
|
||||
|
||||
func (self *BianApiService) {{servicesign}} {
|
||||
|
||||
self.Lock.Lock()
|
||||
defer self.Lock.Unlock()
|
||||
|
||||
message := "{{funcName}} service API not implemented"
|
||||
status := "DM00501"
|
||||
return sendGeneralError(ctx, http.StatusNotImplemented, message, status, "Implement the API code", "")
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"time"
|
||||
"net/http"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
|
||||
"{{moduleName}}/api/gen"
|
||||
)
|
||||
|
||||
|
||||
// Service domain health
|
||||
// (GET /health)
|
||||
func (self *BianApiService) GetHealth(ctx echo.Context) error {
|
||||
|
||||
self.Lock.Lock()
|
||||
defer self.Lock.Unlock()
|
||||
|
||||
description := "API for XXX"
|
||||
serviceName := ""
|
||||
|
||||
health := gen.HealthModel{
|
||||
Description: &description,
|
||||
ServiceId: &serviceName,
|
||||
Status: gen.PASS,
|
||||
}
|
||||
|
||||
health.Links = []gen.LinksModel{}
|
||||
|
||||
hmc := self.newHealthModelChecks()
|
||||
|
||||
asAt := time.Now()
|
||||
|
||||
{
|
||||
var items1 []gen.HealthcheckModel
|
||||
status1 := "PASS"
|
||||
cid1 := "local-file-store"
|
||||
cty1 := "datastore"
|
||||
|
||||
item1 := gen.HealthcheckModel{
|
||||
AsAt: &asAt,
|
||||
Status: &status1,
|
||||
ComponentId: &cid1,
|
||||
ComponentType: &cty1,
|
||||
}
|
||||
items1 = append(items1,item1)
|
||||
|
||||
status2 := "PASS"
|
||||
cid2 := "process-server"
|
||||
cty2 := "system"
|
||||
|
||||
item2 := gen.HealthcheckModel{
|
||||
AsAt: &asAt,
|
||||
Status: &status2,
|
||||
ComponentId: &cid2,
|
||||
ComponentType: &cty2,
|
||||
}
|
||||
items1 = append(items1,item2)
|
||||
|
||||
hmc.Set("dependencies", items1)
|
||||
}
|
||||
|
||||
{
|
||||
var items []gen.HealthcheckModel
|
||||
|
||||
status1 := "PASS"
|
||||
cid1 := "process-server"
|
||||
cty1 := "system"
|
||||
observedValue := float32(asAt.Sub(self.StartTime).Seconds())
|
||||
observedUnit := "s"
|
||||
|
||||
item := gen.HealthcheckModel{
|
||||
AsAt: &asAt,
|
||||
Status: &status1,
|
||||
ComponentId: &cid1,
|
||||
ComponentType: &cty1,
|
||||
ObservedValue: &observedValue,
|
||||
ObservedUnit: &observedUnit,
|
||||
}
|
||||
items = append(items,item)
|
||||
|
||||
hmc.Set("uptime", items)
|
||||
}
|
||||
|
||||
health.Checks = hmc
|
||||
|
||||
return ctx.JSON(http.StatusOK, health)
|
||||
}
|
||||
|
||||
|
||||
func (self *BianApiService) newHealthModelChecks() gen.HealthModel_Checks {
|
||||
|
||||
obj := gen.HealthModel_Checks{}
|
||||
return obj
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,175 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
"net/http"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
|
||||
"{{moduleName}}/api/gen"
|
||||
"{{moduleName}}/api/authn51"
|
||||
)
|
||||
|
||||
|
||||
type BianApiService struct {
|
||||
Lock sync.Mutex
|
||||
StartTime time.Time
|
||||
Configuration SoftwareConfiguration
|
||||
Authn *authn51.Authn51
|
||||
DataFolder string
|
||||
StatusCode int
|
||||
}
|
||||
|
||||
func NewMicroService(serverConfig SoftwareConfiguration) (*BianApiService, error) {
|
||||
|
||||
var microservice = BianApiService{
|
||||
StartTime: time.Now(),
|
||||
DataFolder: serverConfig.Folders.Data,
|
||||
StatusCode: 200,
|
||||
}
|
||||
|
||||
microservice.Configuration = serverConfig
|
||||
aut, err := authn51.NewAuthn51(serverConfig.GetConfigurationFile())
|
||||
if (err != nil) {
|
||||
log.Printf("Error creating Authn51: %v", err)
|
||||
}
|
||||
microservice.Authn = aut
|
||||
|
||||
if (microservice.Configuration.Support.Level > 2) {
|
||||
log.Printf("Base path: %s", microservice.Configuration.Server.BasePath)
|
||||
}
|
||||
|
||||
// Prime the security
|
||||
if (!microservice.IsProductionEnvironment()) {
|
||||
token, _ := microservice.Authn.CreateToken(microservice.Authn.Bearer.DefaultUser)
|
||||
log.Printf("token: %s", token)
|
||||
}
|
||||
|
||||
return µservice, nil
|
||||
}
|
||||
|
||||
|
||||
func sendGeneralErrorBrief(ctx echo.Context, code int, message string) error {
|
||||
return sendGeneralError(ctx, code, message, "", "", "")
|
||||
}
|
||||
|
||||
|
||||
func sendGeneralError(ctx echo.Context, code int, message string, status string, resolution string, technical string) error {
|
||||
|
||||
generalError := gen.ErrorModel{
|
||||
Message: &message,
|
||||
Status: &status,
|
||||
Resolution: &resolution,
|
||||
Technical: &technical,
|
||||
}
|
||||
|
||||
return ctx.JSON(code, generalError)
|
||||
}
|
||||
|
||||
func setLinkBrief(href string, rel string) gen.LinksModel {
|
||||
|
||||
mt := "application/json"
|
||||
op := gen.GET
|
||||
|
||||
return setLink(href, rel, mt, op, "")
|
||||
}
|
||||
|
||||
func setLink(href string, rel string, mediaType string, operation gen.LinksModelOperation, caption string) gen.LinksModel {
|
||||
|
||||
linkHref := href
|
||||
linkRel := rel
|
||||
|
||||
mt := mediaType
|
||||
if (mt == "") {
|
||||
mt = "application/json"
|
||||
}
|
||||
op := operation
|
||||
if (op == "") {
|
||||
op = gen.GET
|
||||
}
|
||||
cap := caption
|
||||
|
||||
var link = gen.LinksModel {
|
||||
Href: linkHref,
|
||||
MediaType: &mt,
|
||||
Rel: linkRel,
|
||||
Operation: &op,
|
||||
Caption: &cap,
|
||||
}
|
||||
|
||||
return link
|
||||
}
|
||||
|
||||
|
||||
func (microservice *BianApiService) IsProductionEnvironment() bool {
|
||||
|
||||
if (microservice.Configuration.Environment == "development" || microservice.Configuration.Environment == "testing") {
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (self *BianApiService) RegisterHandlers(e *echo.Echo) {
|
||||
|
||||
gen.RegisterHandlersWithBaseURL(e, self, self.Configuration.Server.BasePath)
|
||||
|
||||
if (!self.IsProductionEnvironment()) {
|
||||
if (self.Configuration.Support.Level > 2) {
|
||||
log.Print("Enabling non production routes for debugging")
|
||||
}
|
||||
e.Static((self.Configuration.Server.BasePath+ "/"), self.Configuration.Folders.Static)
|
||||
e.GET((self.Configuration.Server.BasePath+ "/authn/new-token"), self.NewToken)
|
||||
e.GET((self.Configuration.Server.BasePath+ "/authn/refresh-token"), self.RefreshToken)
|
||||
}
|
||||
|
||||
if (self.Configuration.Support.Level > 2) {
|
||||
log.Print("Echo routes are:")
|
||||
for _, route := range e.Routes() {
|
||||
log.Printf(" Route: %s %s %s", route.Method, route.Path, route.Name)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
func (self *BianApiService) BrowseSkipper(path string) bool {
|
||||
if (!self.IsProductionEnvironment()) {
|
||||
return browseSkipper(self.Configuration.Server.BasePath, path)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (self *BianApiService) NewToken(ctx echo.Context) error {
|
||||
|
||||
if (self.IsProductionEnvironment()) {
|
||||
return ctx.JSON(http.StatusForbidden, "")
|
||||
} else {
|
||||
token, _ := self.Authn.CreateToken(self.Authn.Bearer.DefaultUser)
|
||||
log.Printf("New token: %s", token)
|
||||
|
||||
return ctx.JSON(http.StatusOK, token)
|
||||
}
|
||||
}
|
||||
|
||||
func (self *BianApiService) RefreshToken(ctx echo.Context) error {
|
||||
|
||||
if (self.IsProductionEnvironment()) {
|
||||
return ctx.JSON(http.StatusForbidden, "")
|
||||
} else {
|
||||
ad, err := self.Authn.ExtractTokenMetadata(ctx.Request())
|
||||
if (err != nil) {
|
||||
return ctx.JSON(http.StatusBadRequest, "")
|
||||
} else {
|
||||
token, _ := self.Authn.CreateToken(ad.UserId)
|
||||
log.Printf("Refresh token: %s \n %s", ad.UserId, token )
|
||||
|
||||
return ctx.JSON(http.StatusOK, token)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package api
|
||||
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
func browseSkipper(baseUrl string, path string) bool {
|
||||
|
||||
if strings.HasSuffix(path, ".html") || strings.HasSuffix(path, ".json") {
|
||||
return true
|
||||
}
|
||||
if strings.HasSuffix(path, ".ico") {
|
||||
return true
|
||||
}
|
||||
if path == (baseUrl + "/") {
|
||||
return true
|
||||
}
|
||||
if strings.HasPrefix(path, (baseUrl+"/authn/")) {
|
||||
return true
|
||||
}
|
||||
if strings.HasPrefix(path, (baseUrl+"/assets/")) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package: gen
|
||||
generate:
|
||||
echo-server: true
|
||||
embedded-spec: true
|
||||
output: api/gen/{{appShort}}-server.gen.go
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package: gen
|
||||
generate:
|
||||
models: true
|
||||
output: api/gen/{{appShort}}-type.gen.go
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
"os/signal"
|
||||
"log"
|
||||
"net/http"
|
||||
"context"
|
||||
|
||||
"github.com/getkin/kin-openapi/openapi3"
|
||||
"github.com/deepmap/oapi-codegen/pkg/middleware"
|
||||
"github.com/labstack/echo/v4"
|
||||
echomiddleware "github.com/labstack/echo/v4/middleware"
|
||||
|
||||
"{{moduleName}}/api"
|
||||
"{{moduleName}}/api/gen"
|
||||
"{{moduleName}}/api/errorh"
|
||||
|
||||
)
|
||||
|
||||
|
||||
func Run(e *echo.Echo, addressPort string, timeOut time.Duration, msgLevel int) {
|
||||
|
||||
var runChan = make(chan os.Signal, 1)
|
||||
|
||||
// Set up a context to allow for graceful server shutdowns in the event
|
||||
// of an OS interrupt (defers the cancel just in case)
|
||||
ctx, cancel := context.WithTimeout(
|
||||
context.Background(),
|
||||
timeOut,
|
||||
)
|
||||
defer cancel()
|
||||
|
||||
// Handle ctrl+c/ctrl+x interrupt
|
||||
signal.Notify(runChan, os.Interrupt)
|
||||
|
||||
if (msgLevel > 0) {
|
||||
log.Printf("Server is starting on %s\n", addressPort)
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err := e.Start(addressPort); err != nil {
|
||||
if err == http.ErrServerClosed {
|
||||
// Normal interrupt operation, ignore
|
||||
} else {
|
||||
log.Fatalf("Server failed to start due to err: %v", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
interrupt := <-runChan
|
||||
|
||||
if (msgLevel > 0) {
|
||||
log.Printf("Server is shutting down due to %+v\n", interrupt)
|
||||
}
|
||||
if err := e.Server.Shutdown(ctx); err != nil {
|
||||
if ("interrupt" != fmt.Sprintf("%v", interrupt)) {
|
||||
log.Printf("Server was unable to gracefully shutdown due to err: '%+v'", err)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
func main() {
|
||||
|
||||
configPath := "./config/{{appShort}}.yaml"
|
||||
softwareConfig, err := api.ParseFlags(configPath)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error loading configuration,flags\n")
|
||||
fmt.Fprintf(os.Stderr, "Error detail: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if (softwareConfig.Support.Level > 2) {
|
||||
log.Printf("Message verbosity level: %d", softwareConfig.Support.Level)
|
||||
}
|
||||
|
||||
swagger, err := gen.GetSwagger()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error loading Open API spec\n: %s", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Create an instance of our handler which satisfies the generated interface
|
||||
var microService, _ = api.NewMicroService(*softwareConfig)
|
||||
|
||||
// Adjust servers to fit configuration
|
||||
swagger.Servers = nil
|
||||
server := openapi3.Server{
|
||||
URL: microService.Configuration.Server.BasePath,
|
||||
}
|
||||
swagger.AddServer(&server)
|
||||
|
||||
e := echo.New()
|
||||
|
||||
// Log all requests
|
||||
e.Use(echomiddleware.Logger())
|
||||
// Custom error handling
|
||||
e.HTTPErrorHandler = errorh.NewHttpErrorHandler().Handler
|
||||
|
||||
var options middleware.Options
|
||||
if (!microService.IsProductionEnvironment()) {
|
||||
options.Skipper = func(c echo.Context) bool {
|
||||
return microService.BrowseSkipper(c.Request().URL.Path)
|
||||
}
|
||||
}
|
||||
options.Options.AuthenticationFunc = microService.Authn.AuthnFunc
|
||||
|
||||
// Use our validation middleware to check all requests against the
|
||||
// OpenAPI schema.
|
||||
e.Use(middleware.OapiRequestValidatorWithOptions(swagger, &options))
|
||||
e.Use(echomiddleware.GzipWithConfig(echomiddleware.GzipConfig{
|
||||
Level: 3,
|
||||
}))
|
||||
|
||||
microService.RegisterHandlers(e)
|
||||
|
||||
if (microService.Configuration.Support.Level < 1) {
|
||||
e.HideBanner = true
|
||||
}
|
||||
|
||||
e.Server.ReadTimeout = microService.Configuration.Server.Timeout.Read * time.Second
|
||||
e.Server.WriteTimeout = microService.Configuration.Server.Timeout.Write * time.Second
|
||||
e.Server.IdleTimeout = microService.Configuration.Server.Timeout.Idle * time.Second
|
||||
|
||||
addressPort := fmt.Sprintf("%s:%d", microService.Configuration.Server.Host, microService.Configuration.Server.Port)
|
||||
timeOut := microService.Configuration.Server.Timeout.Server * time.Second
|
||||
// And we serve HTTP until the world ends.
|
||||
Run(e, addressPort, timeOut, microService.Configuration.Support.Level)
|
||||
}
|
||||
Loading…
Reference in New Issue