Fixes, refactor, a lot of things happen

This commit is contained in:
PB
2022-12-02 21:03:08 +01:00
parent 5adafdd8fb
commit 6f7fdad944
30 changed files with 685 additions and 454 deletions

View File

@@ -12,7 +12,7 @@ func init() {
ErrLoadingEnvs = godotenv.Load()
}
func GetEnv(name, defVal string) string {
func GetEnv(name string, defVal string) string { // FIXME defVal and return types
env := os.Getenv(name)
if env == "" {
return defVal

View File

@@ -6,4 +6,5 @@ type AuthLoginRequest struct {
}
type AuthLoginResponse struct {
JWTToken string `json:"jwt_token"`
}

View File

@@ -0,0 +1,5 @@
package definition
type HealthResponse struct {
Status string `json:"status,omitempty"`
}

View File

@@ -1,35 +0,0 @@
package handler
import (
"net/http"
def "git.pbiernat.dev/egommerce/identity-service/internal/app/definition"
"git.pbiernat.dev/egommerce/identity-service/internal/app/service"
)
var AuthLoginHandler *Handler
func init() {
AuthLoginHandler = &Handler{
Handle: AuthLoginHandlerFunc,
Request: &def.AuthLoginRequest{},
Response: &def.AuthLoginResponse{},
}
}
func AuthLoginHandlerFunc(h *Handler, w http.ResponseWriter) (interface{}, int, error) {
var req = h.Request.(*def.AuthLoginRequest)
// u := entity.TestUser
token, err := service.AuthService.Login(req)
if err != nil {
return nil, http.StatusForbidden, err
}
service.AuthService.SetCookie(w, service.AuthService.TokenCookieName, token)
// service.AuthService.SetCookie(w, service.AuthService.RefreshTokenCookieName, refreshTtoken)
// log.Println("user:", u, "req:", token, "err:", err)
return nil, http.StatusOK, nil
}

View File

@@ -1,18 +1 @@
package handler
import (
"errors"
"net/http"
)
type NotFoundHandler struct{}
func (NotFoundHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
encodeResponse(w, &response{http.StatusNotFound, ""}, errors.New("Path "+r.RequestURI+" not found"))
}
type MethodNotAllowedHandler struct{}
func (MethodNotAllowedHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
encodeResponse(w, &response{http.StatusMethodNotAllowed, ""}, errors.New("Method Not Allowed: "+r.Method))
}

View File

@@ -1,83 +0,0 @@
package handler
import (
"bytes"
"encoding/json"
"io"
"log"
"net/http"
def "git.pbiernat.dev/egommerce/identity-service/internal/app/definition"
"github.com/gorilla/mux"
"github.com/jackc/pgx/v4/pgxpool"
)
type Env struct {
Addr string
DB *pgxpool.Pool
}
type Handler struct {
*Env
Handle HandlerFunc
Request interface{}
Response interface{}
Params Set
}
type HandlerFunc func(h *Handler, w http.ResponseWriter) (interface{}, int, error)
type Set map[string]string
type response struct {
Status int
Data interface{}
}
func Init(e *Env, h *Handler) *Handler {
// return &Handler{e, h.Handle, h.Request, h.Response, Set{}}
h.Env = e
return h
}
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if err := decodeRequestData(r, h.Request); err != nil {
log.Println("Decode request data error:", err.Error())
w.WriteHeader(http.StatusInternalServerError)
}
h.Params = mux.Vars(r)
res, code, err := h.Handle(h, w)
encodeResponse(w, &response{code, res}, err)
}
func decodeRequestData(r *http.Request, v interface{}) error {
buf, _ := io.ReadAll(r.Body)
rdr := io.NopCloser(bytes.NewReader(buf))
r.Body = io.NopCloser(bytes.NewReader(buf))
json.NewDecoder(rdr).Decode(&v)
return nil
}
func encodeResponse(w http.ResponseWriter, res *response, e error) {
if e != nil {
encodeError(w, res.Status, e)
return
}
w.WriteHeader(res.Status)
if res.Data != nil {
json.NewEncoder(w).Encode(res.Data)
}
}
func encodeError(w http.ResponseWriter, status int, e error) {
w.WriteHeader(status)
json.NewEncoder(w).Encode(def.Error(e.Error()))
}

View File

@@ -1,42 +0,0 @@
package handler
import (
"net/http"
)
var HealthCheckHandler *Handler
func init() {
HealthCheckHandler = &Handler{
Handle: HealthCheckHandlerFunc,
Request: &HealthCheckRequest{},
Response: &HealthCheckResponse{},
}
}
type HealthCheckRequest struct {
}
type HealthCheckResponse struct {
Status string `json:"status"`
Data *HealthCheckResponseBody `json:"data"`
}
type HealthCheckResponseBody struct {
Message string `json:"message,omitempty"`
Status string `json:"status,omitempty"`
}
func HealthCheckHandlerFunc(_ *Handler, w http.ResponseWriter) (interface{}, int, error) {
return &HealthCheckResponseBody{
Message: "This is welcome health message. Everything seems to be alright ;)",
Status: "OK",
}, http.StatusOK, nil
// return &HealthCheckResponse{
// Status: http.StatusText(http.StatusOK),
// Data: &HealthCheckResponseBody{
// Message: "This is welcome health message. Everything seems to be alright ;)",
// },
// }, http.StatusOK, nil
}

View File

@@ -0,0 +1,10 @@
package handler
import (
"github.com/gofiber/fiber/v2"
)
func SecretHandler(c *fiber.Ctx) error {
return nil
}

View File

@@ -1,16 +1,20 @@
package app
import "log"
import (
"log"
)
const AppName = "identity-svc"
func Panic(v ...any) {
log.Panicln(Name+":", v)
log.Panicln(AppName+":", v)
}
func Panicf(format string, v ...any) {
log.Panicf(Name+": "+format, v...)
log.Panicf(AppName+": "+format, v...)
}
func Panicln(v ...any) {
v = append([]any{Name + ":"}, v...)
v = append([]any{AppName + ":"}, v...)
log.Panicln(v...)
}

View File

@@ -0,0 +1,37 @@
package middleware
import (
"os"
"github.com/gofiber/fiber/v2"
jwtMiddleware "github.com/gofiber/jwt/v2"
)
// JWTProtected func for specify routes group with JWT authentication.
// See: https://github.com/gofiber/jwt
func JWTProtected() func(*fiber.Ctx) error {
// Create config for JWT authentication middleware.
config := jwtMiddleware.Config{
SigningKey: []byte(os.Getenv("JWT_SECRET_KEY")),
ContextKey: "jwt", // used in private routes
ErrorHandler: jwtError,
}
return jwtMiddleware.New(config)
}
func jwtError(c *fiber.Ctx, err error) error {
// Return status 400 Bad Request and failed authentication error.
if err.Error() == "Missing or malformed JWT" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": true,
"msg": err.Error(),
})
}
// Return status 401 Unauthorized and failed authentication error.
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": true,
"msg": err.Error(),
})
}

View File

@@ -1,23 +0,0 @@
package app
import (
"net/http"
"git.pbiernat.dev/egommerce/identity-service/internal/app/handler"
"github.com/gorilla/mux"
)
func SetupRouter(env *handler.Env) *mux.Router {
r := mux.NewRouter()
r.NotFoundHandler = &handler.NotFoundHandler{}
r.MethodNotAllowedHandler = &handler.MethodNotAllowedHandler{}
r.Use(PrepareHeadersMiddleware)
r.Use(ValidateJsonBodyMiddleware) // probably not needed
r.Use(LoggingMiddleware)
r.Handle("/health", handler.Init(env, handler.HealthCheckHandler)).Methods(http.MethodGet)
r.Handle("/login", handler.Init(env, handler.AuthLoginHandler)).Methods(http.MethodPost)
return r
}

View File

@@ -1,85 +0,0 @@
package app
import (
"bytes"
"encoding/json"
"io"
"log"
"net"
"net/http"
"os"
"strconv"
"time"
def "git.pbiernat.dev/egommerce/identity-service/internal/app/definition"
"git.pbiernat.dev/egommerce/identity-service/internal/app/handler"
)
const Name = "REST API Service"
type Server struct {
*http.Server
}
func NewServer(env *handler.Env) *Server {
return &Server{
&http.Server{
Handler: SetupRouter(env),
Addr: env.Addr,
WriteTimeout: 15 * time.Second,
ReadTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
},
}
}
func (s *Server) Start() {
if os.Getenv("LISTEN_PID") == strconv.Itoa(os.Getpid()) {
// systemd run
f := os.NewFile(3, "from systemd")
l, err := net.FileListener(f)
if err != nil {
log.Fatalln(err)
}
log.Println("Server listening on " + l.Addr().String())
s.Serve(l)
} else {
log.Println("Server listening on " + s.Addr)
log.Fatalln(s.ListenAndServe())
}
}
func PrepareHeadersMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Header().Set("Connection", "keep-alive")
w.Header().Set("Keep-Alive", "timeout=5")
next.ServeHTTP(w, r)
})
}
func ValidateJsonBodyMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
buf, _ := io.ReadAll(r.Body)
r.Body = io.NopCloser(bytes.NewReader(buf)) // rollack *Request to original state
if len(buf) > 0 && !json.Valid(buf) {
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(def.Error("Unable to parse JSON: " + string(buf)))
return
}
next.ServeHTTP(w, r)
})
}
func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Println("Request: " + r.RequestURI + " remote: " + r.RemoteAddr + " via: " + r.UserAgent())
next.ServeHTTP(w, r)
})
}

View File

@@ -0,0 +1,22 @@
package server
import "fmt"
type Config struct {
AppID string
AppName string
AppDomain string
NetAddr string
Port int
LoggerAddr string
RegistryAddr string
DbURL string
MongoDbUrl string
EventBusURL string
EventBusExchange string
EventBusQueue string
}
func (c *Config) GetAppFullName() string {
return fmt.Sprintf("%s_%s", c.AppName, c.AppID)
}

View File

@@ -0,0 +1,12 @@
package server
import (
def "git.pbiernat.dev/egommerce/identity-service/internal/app/definition"
"github.com/gofiber/fiber/v2"
)
func (s *Server) HealthHandler(c *fiber.Ctx) error {
return c.JSON(&def.HealthResponse{
Status: "OK",
})
}

View File

@@ -0,0 +1,24 @@
package server
import (
def "git.pbiernat.dev/egommerce/identity-service/internal/app/definition"
"git.pbiernat.dev/egommerce/identity-service/internal/app/service"
"github.com/gofiber/fiber/v2"
)
func (s *Server) LoginHandler(c *fiber.Ctx) error {
data := new(def.AuthLoginRequest)
if err := c.BodyParser(data); err != nil {
return err
}
token, err := service.AuthService.Login(data.Username, data.Password)
if err != nil {
return fiber.NewError(fiber.StatusUnauthorized)
}
cookie := service.AuthService.Cookie("auth_token", token)
c.Cookie(cookie)
return c.JSON(&def.AuthLoginResponse{JWTToken: token})
}

View File

@@ -0,0 +1,30 @@
package server
import (
"git.pbiernat.dev/egommerce/go-api-pkg/fluentd"
"github.com/gofiber/fiber/v2"
)
func SetupRoutes(s *Server) {
s.App.Get("/health", s.HealthHandler)
api := s.App.Group("/api")
v1 := api.Group("/v1")
v1.Post("/login", s.LoginHandler)
}
func SetupMiddlewares(s *Server) {
s.App.Use(LoggingMiddleware(s.log))
}
// Middlewares
func LoggingMiddleware(log *fluentd.Logger) func(c *fiber.Ctx) error {
return func(c *fiber.Ctx) error {
log.Log("Request: %s, remote: %s, via: %s",
c.Request().URI().String(),
c.Context().RemoteIP().String(),
string(c.Context().UserAgent()))
return c.Next()
}
}

View File

@@ -0,0 +1,111 @@
package server
import (
"os"
"os/signal"
"syscall"
"time"
"github.com/gofiber/fiber/v2"
"github.com/jackc/pgx/v4/pgxpool"
discovery "git.pbiernat.dev/egommerce/go-api-pkg/consul"
"git.pbiernat.dev/egommerce/go-api-pkg/fluentd"
)
type Server struct {
*fiber.App
log *fluentd.Logger
db *pgxpool.Pool
// ebCh *amqp.Channel
discovery *discovery.Service
name string
addr string
}
type Headers struct {
RequestID string `reqHeader:"x-request-id"`
}
func NewServer(conf *Config, logger *fluentd.Logger, db *pgxpool.Pool /*, ebCh *amqp.Channel*/) *Server {
logger.Log("API_ID: %s", conf.AppID)
discovery, err := discovery.NewService(conf.RegistryAddr, conf.AppID, conf.AppName, conf.AppID, conf.AppDomain, conf.Port)
if err != nil {
logger.Log("Error connecting to %s: %v", conf.RegistryAddr, err)
}
logger.Log("Registering service with name: %s, address: %s", discovery.Name, discovery.Address)
err = discovery.Register()
if err != nil {
logger.Log("register error: %v", err)
}
cnf := fiber.Config{
AppName: conf.AppName,
ServerHeader: conf.AppName,
ReadTimeout: time.Millisecond * 50,
WriteTimeout: time.Millisecond * 50,
IdleTimeout: time.Millisecond * 50,
}
s := &Server{
fiber.New(cnf),
logger,
db,
/*ebCh,*/
discovery,
conf.AppName,
conf.NetAddr,
}
SetupMiddlewares(s)
SetupRoutes(s)
return s
}
func (s *Server) Start() {
err := s.Listen(s.addr)
s.log.Log("Starting error: %v", err)
}
func (s *Server) StartWithGracefulShutdown(forever chan struct{}) {
go func() {
sigint := make(chan os.Signal, 1)
signal.Notify(sigint, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
<-sigint
if err := s.gracefulShutdown(); err != nil {
s.log.Log("Server is not shutting down! Reason: %v", err)
}
close(forever)
}()
if err := s.Listen(s.addr); err != nil {
s.log.Log("Server is not running! Reason: %v", err)
}
<-forever
}
// GetRequestID Return current requets ID - works only when fiber context are running
func (s *Server) GetRequestID(c *fiber.Ctx) (string, error) {
var hdr = new(Headers)
if err := c.ReqHeaderParser(hdr); err != nil {
return "", err
}
return hdr.RequestID, nil
}
func (s *Server) gracefulShutdown() error {
s.log.Log("Server is going down...")
s.log.Log("Unregistering service: %s", s.discovery.GetID())
s.discovery.Unregister()
// s.ebCh.Close()
s.db.Close()
s.log.Close()
return s.Shutdown()
}

View File

@@ -1,113 +1,50 @@
package service
import (
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
"strconv"
"time"
"git.pbiernat.dev/egommerce/identity-service/internal/app/config"
def "git.pbiernat.dev/egommerce/identity-service/internal/app/definition"
"github.com/golang-jwt/jwt"
"github.com/gofiber/fiber/v2"
)
var (
AuthService *Auth
JWTService *JWT
ErrUserNotFound = errors.New("user not found")
ErrTokenError = errors.New("failed to generate JWT token")
ErrLoginIncorrect = errors.New("login incorrect")
)
func init() {
expire, _ := strconv.Atoi(config.GetEnv("AUTH_TOKEN_EXPIRE_TIME", "5"))
secret := []byte(config.GetEnv("AUTH_SECRET_HMAC", "B413IlIv9nKQfsMCXTE0Cteo4yHgUEfqaLfjg73sNlh"))
AuthService = &Auth{expire, "jwt_token", "jwt_token_refresh", secret}
cookieExpireTime, _ := strconv.Atoi(config.GetEnv("AUTH_COOKIE_EXPIRE_TIME", "5"))
AuthService = &Auth{"jwt_token", "jwt_token_refresh", cookieExpireTime}
}
type Auth struct {
ExpireTime int // token expire time in minutes
TokenCookieName string
RefreshTokenCookieName string
secret []byte // signing key
cookieExpireTime int
}
func (a *Auth) Login(r *def.AuthLoginRequest) (string, error) {
if r.Username == "admin" && r.Password == "secret" {
token, err := a.createToken()
func (a *Auth) Login(login, pass string) (string, error) {
if login == "admin" && pass == "secret" {
token, err := JWTService.CreateToken()
if err != nil {
return "", ErrTokenError
return "", err
}
return token, nil
}
return "", ErrUserNotFound
return "", ErrLoginIncorrect
}
// SetCookie appends cookie header to response
func (a *Auth) SetCookie(w http.ResponseWriter, name, token string) {
c := &http.Cookie{
// Cookie create fiber.Cookie struct
func (a *Auth) Cookie(name, value string) *fiber.Cookie {
return &fiber.Cookie{
Name: name,
Value: token,
MaxAge: a.ExpireTime * 60,
Path: "/",
Value: value,
MaxAge: a.cookieExpireTime * 300, // FIXME: env/config
Path: "/", // FIXME: env/config
}
http.SetCookie(w, c)
}
func (a Auth) createToken() (string, error) {
// log.Println("now:", time.Now().Unix())
// log.Println("expire at:", time.Now().Add(time.Duration(a.ExpireTime)*time.Minute).Unix())
claims := &jwt.StandardClaims{
ExpiresAt: time.Now().Add(time.Duration(a.ExpireTime) * time.Minute).Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(a.secret)
}
func (a *Auth) validateToken(tokenStr string) error {
token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) {
// Don't forget to validate the alg is what you expect:
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
// hmacSampleSecret is a []byte containing your secret, e.g. []byte("my_secret_key")
return a.secret, nil
})
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
log.Println(claims)
} else {
return err
}
return nil
}
func (a Auth) ValidateUserTokenMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
cToken, err := r.Cookie(a.TokenCookieName)
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(def.Error("Missing JWT Token cookie"))
return
}
if err := a.validateToken(cToken.Value); err != nil {
w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(def.Error(err.Error()))
return
}
next.ServeHTTP(w, r)
})
}

View File

@@ -0,0 +1,53 @@
package service
import (
"fmt"
"strconv"
"time"
"git.pbiernat.dev/egommerce/identity-service/internal/app/config"
"github.com/golang-jwt/jwt"
)
var (
tokenExpireTime int
tokenSecret []byte
)
func init() {
tokenExpireTime, _ = strconv.Atoi(config.GetEnv("JWT_TOKEN_EXPIRE_TIME", "5"))
tokenSecret = []byte(config.GetEnv("JWT_SECRET_KEY", "B413IlIv9nKQfsMCXTE0Cteo4yHgUEfqaLfjg73sNlh")) // FIXME env: JWT_SECRET_KEY !!!
JWTService = &JWT{tokenExpireTime, tokenSecret}
}
type JWT struct {
tokenExpireTime int
tokenSecret []byte
}
func (s *JWT) CreateToken() (string, error) {
claims := &jwt.StandardClaims{
ExpiresAt: time.Now().Add(time.Duration(s.tokenExpireTime) * time.Minute).Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(s.tokenSecret)
}
func (s *JWT) ValidateToken(tokenStr string) error {
token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) {
// Don't forget to validate the alg is what you expect:
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return s.tokenSecret, nil
})
if _, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
return nil
}
return err
}