From 57ab820693e7cb0f489711eee82f97e609acb012 Mon Sep 17 00:00:00 2001 From: Lilleman Date: Thu, 24 Feb 2022 23:46:24 +0100 Subject: [PATCH] Working registration --- .env_example | 2 +- go.mod | 1 + go.sum | 2 + src/api/api.go | 162 ++++++++++++++++++++++++++++++++++ src/api/types.go | 36 ++++++++ src/handlers/register.go | 57 ++++++------ src/handlers/types.go | 5 +- src/main.go | 17 +++- src/view-utils/vutils.go | 12 +++ src/view-utils/vutils_test.go | 25 ++++++ src/views/register.go | 16 +++- 11 files changed, 299 insertions(+), 36 deletions(-) create mode 100644 src/api/api.go create mode 100644 src/api/types.go create mode 100644 src/view-utils/vutils.go create mode 100644 src/view-utils/vutils_test.go diff --git a/.env_example b/.env_example index e486177..a2e5183 100644 --- a/.env_example +++ b/.env_example @@ -1,4 +1,4 @@ ADMIN_API_KEY=changeMe -AUTH_API_URL=http://auth-api:4000 +API_URL=http://auth-api:4000 JWT_SHARED_SECRET=changeMe PORT=4001 \ No newline at end of file diff --git a/go.mod b/go.mod index bcdf642..5abd583 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( require ( github.com/andybalholm/brotli v1.0.4 // indirect + github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/klauspost/compress v1.14.3 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.33.0 // indirect diff --git a/go.sum b/go.sum index 01ce3eb..b2f229c 100644 --- a/go.sum +++ b/go.sum @@ -135,6 +135,8 @@ github.com/gofiber/template v1.6.23 h1:rxIzrukkFrRiC22Z/WRwuySU2z09m932/RkVMAuwc github.com/gofiber/template v1.6.23/go.mod h1:OpKYcUcfli731QNdeN8Y/EkIdKIzN6zenwOj2JrL/pg= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +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/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= diff --git a/src/api/api.go b/src/api/api.go new file mode 100644 index 0000000..c6ac879 --- /dev/null +++ b/src/api/api.go @@ -0,0 +1,162 @@ +package api + +import ( + "bytes" + "encoding/json" + "errors" + "io/ioutil" + "net/http" + "strconv" + + "github.com/golang-jwt/jwt" +) + +// Method must be a valid REST method, like GET, POST, PUT, DELETE etc +func (api Api) Call(method string, path string, jsonPayload []byte) (ApiRes, error) { + respObj := ApiRes{} + + adminToken, _, err := api.getAdminToken() + if err != nil { + return ApiRes{}, err + } + + req, err := http.NewRequest(method, api.URL+path, bytes.NewBuffer(jsonPayload)) + if err != nil { + api.Log.Error("Could not create request", "method", method, "err", err.Error()) + return ApiRes{}, err + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "bearer "+adminToken) + + httpClient := &http.Client{} + res, err := httpClient.Do(req) + if err != nil { + api.Log.Error("Could not call backend API", "err", err.Error()) + return ApiRes{}, err + } + defer res.Body.Close() + + api.Log.Debug("API res status", "status", res.StatusCode) + respObj.StatusCode = res.StatusCode + + if strconv.Itoa(res.StatusCode)[0:1] == "5" { + api.Log.Error("API gave internal server error", "statusCode", res.StatusCode) + return ApiRes{}, errors.New("API gave internal server error") + } else if res.StatusCode == 403 { + api.Log.Error("API responded with 403, Forbidden") + return ApiRes{}, errors.New("API responded with 403, Forbidden") + } else if res.StatusCode == 401 { + api.Log.Error("API responded with 401, Unauthorized") + return ApiRes{}, errors.New("API responded with 403, Unauthorized") + } + + body, err := ioutil.ReadAll(res.Body) + if err != nil { + api.Log.Error("Could not read res body", "err", err.Error()) + return ApiRes{}, err + } + + respObj.Body = body + + return respObj, nil +} + +func (api Api) getAdminToken() (string, AdminClaims, error) { + if api.AdminToken == "" { + // No adminToken exists, obtain it + + adminToken, err := api.getAdminTokenCall() + api.AdminToken = adminToken + + if err != nil { + api.Log.Error("Could not get admin token") + return "", AdminClaims{}, err + } + + parsedToken, err := api.parseAdminToken(api.AdminToken) + if err != nil { + api.Log.Error("Could not parse admin token") + return "", AdminClaims{}, err + } + + return api.AdminToken, parsedToken, err + } + + parsedToken, err := api.parseAdminToken(api.AdminToken) + if err != nil { + api.Log.Error("Could not parse admin token") + return "", AdminClaims{}, err + } + + return api.AdminToken, parsedToken, nil +} + +func (api Api) getAdminTokenCall() (string, error) { + req, err := http.NewRequest("POST", api.URL+"/auth/api-key", bytes.NewBufferString("\""+api.AdminApiKey+"\"")) + if err != nil { + api.Log.Error("Could not create request", "err", err.Error()) + return "", err + } + req.Header.Set("Content-Type", "application/json") + + httpClient := &http.Client{} + res, err := httpClient.Do(req) + if err != nil { + api.Log.Error("Could not call backend API", "err", err.Error()) + return "", err + } + defer res.Body.Close() + + api.Log.Debug("API res status", "status", res.StatusCode) + + if strconv.Itoa(res.StatusCode)[0:1] == "5" { + api.Log.Error("API gave internal server error", "statusCode", res.StatusCode) + return "", errors.New("API gave internal server error") + } else if res.StatusCode != 200 { + api.Log.Error("API gave unexpected statusCode", "statusCode", res.StatusCode) + return "", errors.New("API gave unexpected statusCode") + } + + body, err := ioutil.ReadAll(res.Body) + if err != nil { + api.Log.Error("Could not read res body", "err", err.Error()) + return "", err + } + + var jsonResp TokenRes + + err = json.Unmarshal(body, &jsonResp) + if err != nil { + api.Log.Error("Could not parse JSON from API", "err", err.Error()) + return "", err + } + + return jsonResp.Jwt, nil +} + +func (api Api) parseAdminToken(token string) (AdminClaims, error) { + claims := &AdminClaims{} + parsedToken, err := jwt.ParseWithClaims(token, claims, func(token *jwt.Token) (interface{}, error) { + return []byte(api.JwtSharedSecret), nil + }) + + if err != nil { + api.Log.Error("Could not parse admin token", "err", err.Error()) + return AdminClaims{}, err + } + + if !parsedToken.Valid { + err := errors.New("invalid token") + api.Log.Error("Invalid token") + return AdminClaims{}, err + } + + claims, ok := parsedToken.Claims.(*AdminClaims) + if !ok { + err := errors.New("parsedToken.Claims.(*AdminClaims) did not return ok") + api.Log.Error(err.Error()) + return AdminClaims{}, err + } + + return *claims, nil +} diff --git a/src/api/types.go b/src/api/types.go new file mode 100644 index 0000000..d19c993 --- /dev/null +++ b/src/api/types.go @@ -0,0 +1,36 @@ +package api + +import ( + "github.com/golang-jwt/jwt" + "go.uber.org/zap" +) + +type Api struct { + AdminApiKey string + AdminToken string + JwtSharedSecret string + Log *zap.SugaredLogger + URL string +} + +type ApiRes struct { + Body []byte + StatusCode int +} + +type ApiResError struct { + Error string `json:"error"` + Field string `json:"field"` +} + +type TokenRes struct { + Jwt string `json:"jwt"` + RenewalToken string `json:"renewalToken"` +} + +type AdminClaims struct { + AccountID string `json:"accountId"` + AccountFields map[string][]string `json:"accountFields"` + AccountName string `json:"accountName"` + jwt.StandardClaims +} diff --git a/src/handlers/register.go b/src/handlers/register.go index ced3bf5..ec06a4e 100644 --- a/src/handlers/register.go +++ b/src/handlers/register.go @@ -1,26 +1,25 @@ package handlers import ( - "bytes" "encoding/json" - "fmt" - "io/ioutil" - "net/http" + "strconv" "github.com/gofiber/fiber/v2" + "gitlab.larvit.se/power-plan/auth-ui/src/api" "gitlab.larvit.se/power-plan/auth-ui/src/views" ) func (h Handlers) Register(c *fiber.Ctx) error { c.Set(fiber.HeaderContentType, fiber.MIMETextHTML) - return c.SendString(views.Register()) + return c.SendString(views.Register(views.RegisterData{})) } type NewUser struct { - Username string - Password string - RepeatPassword string + Name string `json:"name"` + Username string `json:"username"` + Password string `json:"password"` + RepeatPassword string `json:"-"` } func (h Handlers) RegisterPost(c *fiber.Ctx) error { @@ -32,8 +31,7 @@ func (h Handlers) RegisterPost(c *fiber.Ctx) error { h.Log.Debug("Invalid input data", "err", err.Error()) c.Status(400) } - - fmt.Printf("newUser: %v", newUser) + newUser.Name = newUser.Username jsonPayload, err := json.Marshal(newUser) if err != nil { @@ -41,30 +39,33 @@ func (h Handlers) RegisterPost(c *fiber.Ctx) error { return c.Status(500).SendString(views.Error500()) } - req, err := http.NewRequest("POST", "http://localhost:4000/account", bytes.NewBuffer(jsonPayload)) - if err != nil { - h.Log.Error("Could not create POST request", "err", err.Error()) - return c.Status(500).SendString(views.Error500()) - } - req.Header.Set("Content-Type", "application/json") - - httpClient := &http.Client{} - res, err := httpClient.Do(req) + apiRes, err := h.Api.Call("POST", "/account", jsonPayload) if err != nil { h.Log.Error("Could not call backend API", "err", err.Error()) return c.Status(500).SendString(views.Error500()) } - defer res.Body.Close() - h.Log.Debug("API res status", "status", res.Status) + h.Log.Debug("API res status", "status", apiRes.StatusCode) - body, err := ioutil.ReadAll(res.Body) - if err != nil { - h.Log.Error("Could not read res body", "err", err.Error()) + if apiRes.StatusCode == 201 { + return c.SendString(views.Register(views.RegisterData{ + OkMsg: "New user created!", + })) + } else if strconv.Itoa(apiRes.StatusCode)[0:1] == "4" { + var jsonResp []api.ApiResError + + err := json.Unmarshal(apiRes.Body, &jsonResp) + if err != nil { + h.Log.Error("Could not unmarshal api error response", "err", err.Error()) + return c.Status(500).SendString(views.Error500()) + } + + return c.Status(apiRes.StatusCode).SendString(views.Register(views.RegisterData{ + ErrField: jsonResp[0].Field, + ErrMsg: jsonResp[0].Error, + })) + } else { + h.Log.Error("Unexpected API response status", "status", apiRes.StatusCode) return c.Status(500).SendString(views.Error500()) } - - fmt.Println("response Body:", string(body)) - - return c.SendString(views.Register()) } diff --git a/src/handlers/types.go b/src/handlers/types.go index cfb104a..e16eb12 100644 --- a/src/handlers/types.go +++ b/src/handlers/types.go @@ -2,11 +2,12 @@ package handlers import ( // jwt "github.com/dgrijalva/jwt-go" + "gitlab.larvit.se/power-plan/auth-ui/src/api" "go.uber.org/zap" ) // Handlers is the overall struct for all http request handlers type Handlers struct { - JwtKey []byte - Log *zap.SugaredLogger + Api api.Api + Log *zap.SugaredLogger } diff --git a/src/main.go b/src/main.go index 3e57daa..cf67a06 100644 --- a/src/main.go +++ b/src/main.go @@ -6,6 +6,7 @@ import ( "github.com/gofiber/fiber/v2" "github.com/joho/godotenv" + "gitlab.larvit.se/power-plan/auth-ui/src/api" h "gitlab.larvit.se/power-plan/auth-ui/src/handlers" "gitlab.larvit.se/power-plan/auth-ui/src/utils" ) @@ -24,11 +25,23 @@ func main() { if os.Getenv("ADMIN_API_KEY") == "changeMe" { log.Error("ADMIN_API_KEY ENV is not set, using very insecure \"changeMe\"") } - jwtKey := []byte(os.Getenv("JWT_SHARED_SECRET")) + jwtSharedSecret := os.Getenv("JWT_SHARED_SECRET") + adminApiKey := os.Getenv("ADMIN_API_KEY") + apiUrl := os.Getenv("API_URL") app := fiber.New() - handlers := h.Handlers{JwtKey: jwtKey, Log: log} + apiInstance := api.Api{ + AdminApiKey: adminApiKey, + JwtSharedSecret: jwtSharedSecret, + Log: log, + URL: apiUrl, + } + + handlers := h.Handlers{ + Api: apiInstance, + Log: log, + } // Log all requests app.Use(handlers.LogReq) diff --git a/src/view-utils/vutils.go b/src/view-utils/vutils.go new file mode 100644 index 0000000..79e2fb5 --- /dev/null +++ b/src/view-utils/vutils.go @@ -0,0 +1,12 @@ +package vu // View Utils + +// Replacement for ternary. Use like: +// str := "Foo " + Tern(len("") != 0, "bar", "baz") + " yes" // Evaluates to: "Foo baz yes" +// str := "Foo " + Tern(true, "bar", "baz") + " yes" // Evaluates to: "Foo bar yes" +func TernStr(exp bool, expTrue string, expFalse string) string { + if exp { + return expTrue + } else { + return expFalse + } +} diff --git a/src/view-utils/vutils_test.go b/src/view-utils/vutils_test.go new file mode 100644 index 0000000..69527d4 --- /dev/null +++ b/src/view-utils/vutils_test.go @@ -0,0 +1,25 @@ +package vu + +import "testing" + +func TestTern(t *testing.T) { + test1 := "a" + TernStr(1 == 2, "b", "c") + "d" + if test1 != "acd" { + t.Fatalf("Expected \"acd\" but got %v", test1) + } + + test2 := "a" + TernStr(1 == 1, "b", "c") + "d" + if test2 != "abd" { + t.Fatalf("Expected \"abd\" but got %v", test1) + } + + test3 := "a" + TernStr(false, "b", "c") + "d" + if test3 != "acd" { + t.Fatalf("Expected \"acd\" but got %v", test1) + } + + test4 := "a" + TernStr(true, "b", "c") + "d" + if test4 != "abd" { + t.Fatalf("Expected \"abd\" but got %v", test1) + } +} diff --git a/src/views/register.go b/src/views/register.go index 5c7767d..3318755 100644 --- a/src/views/register.go +++ b/src/views/register.go @@ -1,11 +1,21 @@ package views -import "gitlab.larvit.se/power-plan/auth-ui/src/views/layouts" +import ( + vu "gitlab.larvit.se/power-plan/auth-ui/src/view-utils" + "gitlab.larvit.se/power-plan/auth-ui/src/views/layouts" +) -func Register() string { +type RegisterData struct { + ErrField string + ErrMsg string + OkMsg string +} + +func Register(data RegisterData) string { content := `

Register

+ ` + vu.TernStr(len(data.OkMsg) != 0, "

"+data.OkMsg+" Login

", "") + `
@@ -21,7 +31,7 @@ func Register() string {
- + ` + vu.TernStr(len(data.ErrMsg) != 0, "

ERROR! "+data.ErrMsg+"

", "") + `

Cancel