[feature] Merged with api-prototype
This commit is contained in:
parent
748b631d0c
commit
c44ec98e39
2
.env.dist
Normal file
2
.env.dist
Normal file
@ -0,0 +1,2 @@
|
||||
HTTP_PORT=8000
|
||||
DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5432/svc_identity
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -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/
|
||||
|
||||
|
45
cmd/main.go
45
cmd/main.go
@ -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)
|
||||
}
|
||||
|
22
internal/app/config/env.go
Normal file
22
internal/app/config/env.go
Normal 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
|
||||
}
|
16
internal/app/database/connect.go
Normal file
16
internal/app/database/connect.go
Normal 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
|
||||
}
|
9
internal/app/definition/auth.go
Normal file
9
internal/app/definition/auth.go
Normal file
@ -0,0 +1,9 @@
|
||||
package definition
|
||||
|
||||
type AuthLoginRequest struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type AuthLoginResponse struct {
|
||||
}
|
9
internal/app/definition/error.go
Normal file
9
internal/app/definition/error.go
Normal file
@ -0,0 +1,9 @@
|
||||
package definition
|
||||
|
||||
type ErrorResponse struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
func Error(err string) *ErrorResponse {
|
||||
return &ErrorResponse{err}
|
||||
}
|
18
internal/app/entity/user.go
Normal file
18
internal/app/entity/user.go
Normal 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(),
|
||||
}
|
35
internal/app/handler/auth.go
Normal file
35
internal/app/handler/auth.go
Normal 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
|
||||
}
|
15
internal/app/handler/error.go
Normal file
15
internal/app/handler/error.go
Normal 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)
|
||||
}
|
83
internal/app/handler/handler.go
Normal file
83
internal/app/handler/handler.go
Normal 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()))
|
||||
}
|
40
internal/app/handler/health_check.go
Normal file
40
internal/app/handler/health_check.go
Normal 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
16
internal/app/log.go
Normal 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
26
internal/app/router.go
Normal 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
85
internal/app/server.go
Normal 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)
|
||||
})
|
||||
}
|
113
internal/app/service/auth.go
Normal file
113
internal/app/service/auth.go
Normal 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)
|
||||
})
|
||||
}
|
Loading…
Reference in New Issue
Block a user