[feature] Merged with api-prototype #1

Merged
keedosn merged 1 commits from feature/apply-api-prototype into develop 2022-06-19 15:20:39 +02:00
16 changed files with 534 additions and 2 deletions
Showing only changes of commit c44ec98e39 - Show all commits

2
.env.dist Normal file
View File

@ -0,0 +1,2 @@
HTTP_PORT=8000
DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5432/svc_identity

2
.gitignore vendored
View File

@ -12,6 +12,8 @@
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
.env
# Dependency directories (remove the comment below to include it)
# vendor/

View File

@ -1,7 +1,48 @@
package main
import "fmt"
import (
"context"
"git.pbiernat.dev/egommerce/application/services/identity/internal/app"
"git.pbiernat.dev/egommerce/application/services/identity/internal/app/config"
"git.pbiernat.dev/egommerce/application/services/identity/internal/app/database"
"git.pbiernat.dev/egommerce/application/services/identity/internal/app/handler"
"net"
"os"
"os/signal"
"time"
)
const (
defHttpIp = "127.0.0.1"
defHttpPort = "8080"
defDbUrl = "postgres://postgres:postgres@127.0.0.1:5432/egommerce" // FIXME: use env
)
func main() {
fmt.Println("Identity services")
if config.ErrLoadingEnvs != nil {
app.Panicf("Error loading .env file")
}
httpAddr := net.JoinHostPort(config.GetEnv("SERVER_IP", defHttpIp), config.GetEnv("SERVER_PORT", defHttpPort))
dbConnStr := config.GetEnv("DATABASE_URL", defDbUrl)
dbc, err := database.Connect(dbConnStr)
if err != nil {
app.Panicf("Unable to connect to database: %v\n", err)
}
env := &handler.Env{httpAddr, dbc}
srv := app.NewServer(env)
go srv.Start()
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
<-c
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
srv.Shutdown(ctx)
os.Exit(0)
}

View File

@ -0,0 +1,22 @@
package config
import (
"os"
"github.com/joho/godotenv"
)
var ErrLoadingEnvs error
func init() {
ErrLoadingEnvs = godotenv.Load()
}
func GetEnv(name, defVal string) string {
env := os.Getenv(name)
if env == "" {
return defVal
}
return env
}

View File

@ -0,0 +1,16 @@
package database
import (
"context"
"github.com/jackc/pgx/v4/pgxpool"
)
func Connect(connStr string) (*pgxpool.Pool, error) {
conn, err := pgxpool.Connect(context.Background(), connStr)
if err != nil {
return nil, err
}
return conn, nil
}

View File

@ -0,0 +1,9 @@
package definition
type AuthLoginRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}
type AuthLoginResponse struct {
}

View File

@ -0,0 +1,9 @@
package definition
type ErrorResponse struct {
Error string `json:"error"`
}
func Error(err string) *ErrorResponse {
return &ErrorResponse{err}
}

View File

@ -0,0 +1,18 @@
package entity
import "time"
type User struct {
ID int `json:"id"`
Username string `json:"username"`
Password string `json:"password"`
CreateDate time.Time `json:"create_date"`
ModifyDate time.Time `json:"modify_date"` // FIXME: zero-value issue
}
var TestUser = &User{
ID: 1,
Username: "test",
Password: "test",
CreateDate: time.Now(),
}

View File

@ -0,0 +1,35 @@
package handler
import (
"net/http"
def "git.pbiernat.dev/egommerce/application/services/identity/internal/app/definition"
"git.pbiernat.dev/egommerce/application/services/identity/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.StatusUnauthorized, 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

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

View File

@ -0,0 +1,83 @@
package handler
import (
"bytes"
"encoding/json"
"io/ioutil"
"log"
"net/http"
def "git.pbiernat.dev/egommerce/application/services/identity/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, _ := ioutil.ReadAll(r.Body)
rdr := ioutil.NopCloser(bytes.NewReader(buf))
r.Body = ioutil.NopCloser(bytes.NewReader(buf))
json.NewDecoder(rdr).Decode(&v)
return nil
}
func encodeResponse(w http.ResponseWriter, res *response, err error) {
if err != nil {
encodeError(w, res.Status, err)
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

@ -0,0 +1,40 @@
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"`
}
func HealthCheckHandlerFunc(_ *Handler, w http.ResponseWriter) (interface{}, int, error) {
return &HealthCheckResponseBody{
Message: "This is welcome health message. Everything seems to be alright ;)",
}, 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
}

16
internal/app/log.go Normal file
View File

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

26
internal/app/router.go Normal file
View File

@ -0,0 +1,26 @@
package app
import (
"net/http"
"git.pbiernat.dev/egommerce/application/services/identity/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)
hc := r.PathPrefix("/health").Subrouter()
hc.Handle("", handler.Init(env, handler.HealthCheckHandler)).Methods(http.MethodGet)
auth := r.PathPrefix("/auth").Subrouter()
auth.Handle("/login", handler.Init(env, handler.AuthLoginHandler)).Methods(http.MethodPost)
return r
}

85
internal/app/server.go Normal file
View File

@ -0,0 +1,85 @@
package app
import (
"bytes"
"encoding/json"
"io/ioutil"
"log"
"net"
"net/http"
"os"
"strconv"
"time"
def "git.pbiernat.dev/egommerce/application/services/identity/internal/app/definition"
"git.pbiernat.dev/egommerce/application/services/identity/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, _ := ioutil.ReadAll(r.Body)
r.Body = ioutil.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,113 @@
package service
import (
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
"strconv"
"time"
"git.pbiernat.dev/egommerce/application/services/identity/internal/app/config"
def "git.pbiernat.dev/egommerce/application/services/identity/internal/app/definition"
"github.com/golang-jwt/jwt"
)
var (
AuthService *Auth
ErrUserNotFound = errors.New("user not found")
ErrTokenError = errors.New("failed to generate JWT token")
)
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}
}
type Auth struct {
ExpireTime int // token expire time in minutes
TokenCookieName string
RefreshTokenCookieName string
secret []byte // signing key
}
func (a *Auth) Login(r *def.AuthLoginRequest) (string, error) {
if r.Username == "admin" && r.Password == "secret" {
token, err := a.createToken()
if err != nil {
return "", ErrTokenError
}
return token, nil
}
return "", ErrUserNotFound
}
// SetCookie appends cookie header to response
func (a *Auth) SetCookie(w http.ResponseWriter, name, token string) {
c := &http.Cookie{
Name: name,
Value: token,
MaxAge: a.ExpireTime * 60,
Path: "/",
}
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)
})
}