Initial develop branch

This commit is contained in:
Piotr Biernat 2022-03-11 18:08:03 +01:00
parent 1d6f517ad3
commit 3d3ce8873c
20 changed files with 298 additions and 116 deletions

View File

@ -1,2 +1,10 @@
HTTP_PORT=8000
# Server config
SERVER_IP=127.0.0.1
SERVER_PORT=8000
# Auth config
AUTH_SECRET_HMAC=HmacSecretToken
AUTH_TOKEN_EXPIRE_TIME=5
# Database config
DATABASE_URL=postgres://username:password@host:port/db_name

View File

@ -9,24 +9,24 @@ import (
"time"
"git.pbiernat.dev/golang/rest-api-prototype/internal/app"
"git.pbiernat.dev/golang/rest-api-prototype/internal/app/config"
"git.pbiernat.dev/golang/rest-api-prototype/internal/app/database"
"git.pbiernat.dev/golang/rest-api-prototype/internal/app/handler"
"github.com/joho/godotenv"
)
const ( // FIXME
defaultHTTPPort = "8080"
defaultDatabaseUrl = "postgres://postgres:12345678@127.0.0.1:5434/S7"
const (
defHttpIp = "127.0.0.1"
defHttpPort = "8080"
defDbUrl = "postgres://postgres:postgres@127.0.0.1:5432/Api" // FIXME use default container conf in future
)
func main() {
err := godotenv.Load(".env")
if err != nil {
if config.ErrLoadingEnvs != nil {
log.Fatalln("Error loading .env file")
}
httpAddr := net.JoinHostPort("127.0.0.1", getEnv("HTTP_PORT", defaultHTTPPort))
dbConnStr := getEnv("DATABASE_URL", defaultDatabaseUrl)
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 {
@ -48,12 +48,3 @@ func main() {
srv.Shutdown(ctx)
os.Exit(0)
}
func getEnv(name, defVal string) string {
env := os.Getenv(name)
if env == "" {
return defVal
}
return env
}

1
go.mod
View File

@ -4,6 +4,7 @@ go 1.17
require (
github.com/go-ozzo/ozzo-validation v3.6.0+incompatible
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/gorilla/mux v1.8.0
github.com/jackc/pgx/v4 v4.15.0
github.com/joho/godotenv v1.4.0

2
go.sum
View File

@ -17,6 +17,8 @@ github.com/go-ozzo/ozzo-validation v3.6.0+incompatible/go.mod h1:gsEKFIVnabGBt6m
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=

1
insomnia.json Normal file

File diff suppressed because one or more lines are too long

View File

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

View File

@ -0,0 +1,16 @@
package definition
import "git.pbiernat.dev/golang/rest-api-prototype/internal/app/entity"
type CreateArticleRequest struct {
CategoryID int `json:"category_id"`
Title string `json:"title"`
Intro string `json:"intro"`
Text string `json:"text"`
}
type CreateArticleResponse struct {
Status string `json:"status"`
Data *entity.Article `json:"data"`
Err string `json:"err,omitempty"`
}

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,27 @@
package definition
import (
"git.pbiernat.dev/golang/rest-api-prototype/internal/app/entity"
validation "github.com/go-ozzo/ozzo-validation"
)
type CreateCategoryRequest struct {
Name string `json:"name"`
}
func (c CreateCategoryRequest) Validate() error {
return validation.ValidateStruct(&c,
validation.Field(&c.Name, validation.Required, validation.Length(3, 255)),
)
}
type CreateCategoryResponse struct {
Data *entity.Category `json:"data"`
Err string `json:"err,omitempty"` // FIXME: omitempty on/off?
}
type DeleteCategoryRequest struct {
}
type DeleteCategoryResponse 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

@ -8,7 +8,7 @@ type Category struct {
ID int `json:"id"`
Name string `json:"name"`
CreateDate time.Time `json:"create_date"`
ModifyDate time.Time `json:"modify_date"` // FIXME
ModifyDate time.Time `json:"modify_date"` // FIXME: zero-value issue
}
// func (c Category) Validate() error {

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

@ -5,6 +5,7 @@ import (
"net/http"
"time"
def "git.pbiernat.dev/golang/rest-api-prototype/internal/app/definition"
"git.pbiernat.dev/golang/rest-api-prototype/internal/app/entity"
)
@ -13,26 +14,13 @@ var CreateArticleHandler *Handler
func init() {
CreateArticleHandler = &Handler{
Handle: CreateArticleHandlerFunc,
Request: &CreateArticleRequest{},
Response: &CreateArticleResponse{},
Request: &def.CreateArticleRequest{},
Response: &def.CreateArticleResponse{},
}
}
type CreateArticleRequest struct {
CategoryID int `json:"category_id"`
Title string `json:"title"`
Intro string `json:"intro"`
Text string `json:"text"`
}
type CreateArticleResponse struct {
Status string `json:"status"`
Data *entity.Article `json:"data"`
Err string `json:"err,omitempty"`
}
func CreateArticleHandlerFunc(h *Handler, w http.ResponseWriter) (interface{}, int, error) {
var art = h.Request.(*CreateArticleRequest)
var art = h.Request.(*def.CreateArticleRequest)
log.Println(art)
return &entity.Article{
@ -43,16 +31,4 @@ func CreateArticleHandlerFunc(h *Handler, w http.ResponseWriter) (interface{}, i
Text: "Text",
CreateDate: time.Now(),
}, http.StatusCreated, nil
// return &CreateArticleResponse{
// Status: http.StatusText(http.StatusOK),
// Data: &entity.Article{
// ID: 1,
// CategoryID: 1,
// Title: "Dummy article",
// Intro: "Intro",
// Text: "Text",
// CreateDate: time.Now(),
// },
// }, http.StatusCreated, nil
}

View File

@ -0,0 +1,35 @@
package handler
import (
"net/http"
def "git.pbiernat.dev/golang/rest-api-prototype/internal/app/definition"
"git.pbiernat.dev/golang/rest-api-prototype/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

@ -6,8 +6,8 @@ import (
"strconv"
"time"
def "git.pbiernat.dev/golang/rest-api-prototype/internal/app/definition"
"git.pbiernat.dev/golang/rest-api-prototype/internal/app/entity"
validation "github.com/go-ozzo/ozzo-validation"
)
var CreateCategoryHandler *Handler
@ -16,40 +16,19 @@ var DeleteCategoryHandler *Handler
func init() {
CreateCategoryHandler = &Handler{
Handle: CreateCategoryHandlerFunc,
Request: &CreateCategoryRequest{},
Response: &CreateCategoryResponse{},
Request: &def.CreateCategoryRequest{},
Response: &def.CreateCategoryResponse{},
}
DeleteCategoryHandler = &Handler{
Handle: DeleteCategoryHandlerFunc,
Request: &DeleteCategoryRequest{},
Response: &DeleteCategoryResponse{},
Request: &def.DeleteCategoryRequest{},
Response: &def.DeleteCategoryResponse{},
}
}
type CreateCategoryRequest struct {
Name string `json:"name"`
}
func (c CreateCategoryRequest) Validate() error {
return validation.ValidateStruct(&c,
validation.Field(&c.Name, validation.Required, validation.Length(3, 255)),
)
}
type CreateCategoryResponse struct {
Data *entity.Category `json:"data"`
Err string `json:"err,omitempty"` // FIXME: omitempty on/off?
}
type DeleteCategoryRequest struct {
}
type DeleteCategoryResponse struct {
}
func CreateCategoryHandlerFunc(h *Handler, w http.ResponseWriter) (interface{}, int, error) {
var cat = h.Request.(*CreateCategoryRequest)
var cat = h.Request.(*def.CreateCategoryRequest)
log.Println("Cat input:", cat)
if err := cat.Validate(); err != nil {
@ -61,25 +40,18 @@ func CreateCategoryHandlerFunc(h *Handler, w http.ResponseWriter) (interface{},
Name: cat.Name,
CreateDate: time.Now(),
}, http.StatusCreated, nil
// return &CreateCategoryResponse{
// Data: &entity.Category{
// Name: "Dummy category",
// CreateDate: time.Now(),
// },
// }, http.StatusCreated, nil
}
func DeleteCategoryHandlerFunc(h *Handler, w http.ResponseWriter) (interface{}, int, error) {
var cat = h.Request.(*DeleteCategoryRequest)
var cat = h.Request.(*def.DeleteCategoryRequest)
log.Println(cat)
id, _ := strconv.Atoi(h.Params["id"])
log.Println(h.Params)
if id != 1 {
return &DeleteCategoryResponse{}, http.StatusNotFound, nil
return nil, http.StatusNotFound, nil
}
return &DeleteCategoryResponse{}, http.StatusNoContent, nil
return nil, http.StatusNoContent, nil
}

View File

@ -2,14 +2,6 @@ package handler
import "net/http"
type ErrorResponse struct {
Error string `json:"error"`
}
func Error(err string) *ErrorResponse {
return &ErrorResponse{Error: err}
}
type NotFoundHandler struct{}
func (NotFoundHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {

View File

@ -7,6 +7,7 @@ import (
"log"
"net/http"
def "git.pbiernat.dev/golang/rest-api-prototype/internal/app/definition"
"github.com/gorilla/mux"
"github.com/jackc/pgx/v4/pgxpool"
)
@ -39,7 +40,7 @@ func New(e *Env, h *Handler) *Handler {
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if err := decodeRequestData(r, h.Request); err != nil {
log.Println("err_ServeHTTP:", err.Error())
log.Println("Decode request data error:", err.Error())
w.WriteHeader(http.StatusInternalServerError)
}
@ -73,5 +74,5 @@ func encodeResponse(w http.ResponseWriter, res *response, err error) {
func encodeError(w http.ResponseWriter, status int, e error) {
w.WriteHeader(status)
json.NewEncoder(w).Encode(Error(e.Error()))
json.NewEncoder(w).Encode(def.Error(e.Error()))
}

View File

@ -4,6 +4,7 @@ import (
"net/http"
"git.pbiernat.dev/golang/rest-api-prototype/internal/app/handler"
"git.pbiernat.dev/golang/rest-api-prototype/internal/app/service"
"github.com/gorilla/mux"
)
@ -19,7 +20,12 @@ func SetupRouter(env *handler.Env) *mux.Router {
hc := r.PathPrefix("/health").Subrouter()
hc.Handle("", handler.New(env, handler.HealthCheckHandler)).Methods(http.MethodGet)
auth := r.PathPrefix("/auth").Subrouter()
auth.Handle("/login", handler.New(env, handler.AuthLoginHandler)).Methods(http.MethodPost)
api := r.PathPrefix("/api").Subrouter()
api.Use(service.AuthService.ValidateUserTokenMiddleware) // only /api/** endpoints use this middleware
api.Handle("/article", handler.New(env, handler.CreateArticleHandler)).Methods(http.MethodPost)
api.Handle("/category", handler.New(env, handler.CreateCategoryHandler)).Methods(http.MethodPost)

View File

@ -8,6 +8,7 @@ import (
"net/http"
"time"
def "git.pbiernat.dev/golang/rest-api-prototype/internal/app/definition"
"git.pbiernat.dev/golang/rest-api-prototype/internal/app/handler"
)
@ -34,14 +35,6 @@ func (s *Server) Start() {
}
}
// func (s *Server) Shutdown(ctx context.Context) {
// log.Println("Shutting down...")
// if err := s.Shutdown(ctx); err != nil {
// log.Panicln(err)
// }
// }
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")
@ -57,22 +50,9 @@ func ValidateJsonBodyMiddleware(next http.Handler) http.Handler {
buf, _ := ioutil.ReadAll(r.Body)
r.Body = ioutil.NopCloser(bytes.NewReader(buf)) // rollack *Request to original state
// if len(buf) > 0 {
// next.ServeHTTP(w, r)
// return
// }
// if !json.Valid(buf) {
// w.WriteHeader(http.StatusBadRequest)
// json.NewEncoder(w).Encode(handler.Error("Unable to parse JSON: " + string(buf)))
// return
// }
if len(buf) > 0 && !json.Valid(buf) {
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(handler.Error("Unable to parse JSON: " + string(buf)))
json.NewEncoder(w).Encode(def.Error("Unable to parse JSON: " + string(buf)))
return
}

View File

@ -0,0 +1,113 @@
package service
import (
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
"strconv"
"time"
"git.pbiernat.dev/golang/rest-api-prototype/internal/app/config"
def "git.pbiernat.dev/golang/rest-api-prototype/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)
})
}