diff --git a/.env.dist b/.env.dist index f67fe51..9fb4005 100644 --- a/.env.dist +++ b/.env.dist @@ -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 diff --git a/cmd/server/main.go b/cmd/server/main.go index 73b5a92..7804c70 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -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 -} diff --git a/go.mod b/go.mod index 848ae78..091c6c7 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 6b286c5..d390480 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/insomnia.json b/insomnia.json new file mode 100644 index 0000000..90a2aea --- /dev/null +++ b/insomnia.json @@ -0,0 +1 @@ +{"_type":"export","__export_format":4,"__export_date":"2022-03-11T17:07:26.353Z","__export_source":"insomnia.desktop.app:v2022.1.1","resources":[{"_id":"req_58100e5dea474d8c9cf266870fd80377","parentId":"wrk_41519d43247745e8b0d75fca79391efa","modified":1646661433716,"created":1646661433716,"url":"http://localhost:8881/health","name":"Health Check","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1646574517992,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"wrk_41519d43247745e8b0d75fca79391efa","parentId":null,"modified":1646661433686,"created":1646661433686,"name":"My Golang REST API Prototype","description":"","scope":"collection","_type":"workspace"},{"_id":"req_d85f39b62a764c578f802155aa9444eb","parentId":"fld_67b5722ae8844885b1c67d1a2284f8e6","modified":1647018330033,"created":1646661433714,"url":"http://localhost:8881/auth/login","name":"Login","description":"","method":"POST","body":{"mimeType":"application/json","text":"{\n\t\"username\": \"admin\",\n\t\"password\": \"secret\"\n}"},"parameters":[],"headers":[{"name":"Content-Type","value":"application/json","id":"pair_3ea07ffbb4324b20840abe898c0d4414","disabled":false},{"id":"pair_0e029fd7f4024de096264331881a024c","name":"Cookie","value":"jwt_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NDY1OTQwNjd9.cTJjE5x0A8JBw27PedTZU9f-RRsBsdSMxmn-yNFCuuU","description":"","disabled":true}],"authentication":{},"metaSortKey":-1646417255711,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"fld_67b5722ae8844885b1c67d1a2284f8e6","parentId":"wrk_41519d43247745e8b0d75fca79391efa","modified":1646661433714,"created":1646661433714,"name":"Auth","description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1646574517942,"_type":"request_group"},{"_id":"req_2b06897bb38d4b8d932105730ff48ada","parentId":"fld_54ebf6b453c4445d98868b790d3f9854","modified":1646661481164,"created":1646661433707,"url":"http://localhost:8881/api/article","name":"Create","description":"","method":"POST","body":{},"parameters":[],"headers":[{"id":"pair_579e2707274042e384d0e2f32f0c7278","name":"Cookie","value":"jwt_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NDY1OTQwNjd9.cTJjE5x0A8JBw27PedTZU9f-RRsBsdSMxmn-yNFCuuU","description":"","disabled":true}],"authentication":{},"metaSortKey":-1646417255711,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"fld_54ebf6b453c4445d98868b790d3f9854","parentId":"wrk_41519d43247745e8b0d75fca79391efa","modified":1646661433706,"created":1646661433706,"name":"Article","description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1646417248799,"_type":"request_group"},{"_id":"req_348fde0cea6f445ab770e6433aac52d0","parentId":"fld_cf69221444e04ddca57529ba95e7d1c8","modified":1647018360561,"created":1646661433712,"url":"http://localhost:8881/api/category","name":"Create","description":"","method":"POST","body":{"mimeType":"application/json","text":"{\n\t\"name\": \"asd\"\n}"},"parameters":[],"headers":[{"name":"Content-Type","value":"application/json","id":"pair_3ea07ffbb4324b20840abe898c0d4414"}],"authentication":{},"metaSortKey":-1646245285350.375,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"fld_cf69221444e04ddca57529ba95e7d1c8","parentId":"wrk_41519d43247745e8b0d75fca79391efa","modified":1646661433710,"created":1646661433710,"name":"Category","description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1646232558002.5,"_type":"request_group"},{"_id":"req_5d3c3473aa2a4d7e922c3f50708093bc","parentId":"fld_cf69221444e04ddca57529ba95e7d1c8","modified":1646661433711,"created":1646661433711,"url":"http://localhost:8881/api/category/1","name":"Delete","description":"","method":"DELETE","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1646073314989.75,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"env_60920aa357fb443d9eb9047a140594d7","parentId":"wrk_41519d43247745e8b0d75fca79391efa","modified":1646661433689,"created":1646661433689,"name":"Base Environment","data":{},"dataPropertyOrder":null,"color":null,"isPrivate":false,"metaSortKey":1646417242795,"_type":"environment"},{"_id":"jar_e540c6276bec4e05b3dde1b9c048bead","parentId":"wrk_41519d43247745e8b0d75fca79391efa","modified":1647018351627,"created":1646661433698,"name":"Default Jar","cookies":[],"_type":"cookie_jar"},{"_id":"spc_6ec5c1d0a9d14ed2aee3bf4729088a30","parentId":"wrk_41519d43247745e8b0d75fca79391efa","modified":1646661433728,"created":1646661433703,"fileName":"My Golang REST API Prototype","contents":"","contentType":"yaml","_type":"api_spec"}]} \ No newline at end of file diff --git a/internal/app/config/env.go b/internal/app/config/env.go new file mode 100644 index 0000000..bb3742d --- /dev/null +++ b/internal/app/config/env.go @@ -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 +} diff --git a/internal/app/definition/article.go b/internal/app/definition/article.go new file mode 100644 index 0000000..a6b2800 --- /dev/null +++ b/internal/app/definition/article.go @@ -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"` +} diff --git a/internal/app/definition/auth.go b/internal/app/definition/auth.go new file mode 100644 index 0000000..f32ca44 --- /dev/null +++ b/internal/app/definition/auth.go @@ -0,0 +1,9 @@ +package definition + +type AuthLoginRequest struct { + Username string `json:"username"` + Password string `json:"password"` +} + +type AuthLoginResponse struct { +} diff --git a/internal/app/definition/category.go b/internal/app/definition/category.go new file mode 100644 index 0000000..09804ca --- /dev/null +++ b/internal/app/definition/category.go @@ -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 { +} diff --git a/internal/app/definition/error.go b/internal/app/definition/error.go new file mode 100644 index 0000000..28f7dbf --- /dev/null +++ b/internal/app/definition/error.go @@ -0,0 +1,9 @@ +package definition + +type ErrorResponse struct { + Error string `json:"error"` +} + +func Error(err string) *ErrorResponse { + return &ErrorResponse{err} +} diff --git a/internal/app/entity/category.go b/internal/app/entity/category.go index e021298..a91ff4c 100644 --- a/internal/app/entity/category.go +++ b/internal/app/entity/category.go @@ -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 { diff --git a/internal/app/entity/user.go b/internal/app/entity/user.go new file mode 100644 index 0000000..2b0b578 --- /dev/null +++ b/internal/app/entity/user.go @@ -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(), +} diff --git a/internal/app/handler/article.go b/internal/app/handler/article.go index b6e9414..fc2500d 100644 --- a/internal/app/handler/article.go +++ b/internal/app/handler/article.go @@ -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 } diff --git a/internal/app/handler/auth.go b/internal/app/handler/auth.go new file mode 100644 index 0000000..1aa2518 --- /dev/null +++ b/internal/app/handler/auth.go @@ -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 +} diff --git a/internal/app/handler/category.go b/internal/app/handler/category.go index 8da9f1b..b4e5723 100644 --- a/internal/app/handler/category.go +++ b/internal/app/handler/category.go @@ -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 } diff --git a/internal/app/handler/error.go b/internal/app/handler/error.go index 75291c7..540b989 100644 --- a/internal/app/handler/error.go +++ b/internal/app/handler/error.go @@ -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) { diff --git a/internal/app/handler/handler.go b/internal/app/handler/handler.go index 9330243..d7ef416 100644 --- a/internal/app/handler/handler.go +++ b/internal/app/handler/handler.go @@ -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())) } diff --git a/internal/app/router.go b/internal/app/router.go index 78f4bec..3abfe4c 100644 --- a/internal/app/router.go +++ b/internal/app/router.go @@ -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) diff --git a/internal/app/server.go b/internal/app/server.go index e2f59ea..458bf8b 100644 --- a/internal/app/server.go +++ b/internal/app/server.go @@ -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 } diff --git a/internal/app/service/auth.go b/internal/app/service/auth.go new file mode 100644 index 0000000..49bbd9b --- /dev/null +++ b/internal/app/service/auth.go @@ -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) + }) +}