diff --git a/cmd/web-api/handlers/routes.go b/cmd/web-api/handlers/routes.go index 0107be2..c056074 100644 --- a/cmd/web-api/handlers/routes.go +++ b/cmd/web-api/handlers/routes.go @@ -1,6 +1,7 @@ package handlers import ( + "context" "log" "net/http" "os" @@ -10,9 +11,12 @@ import ( "geeks-accelerator/oss/saas-starter-kit/internal/platform/auth" "geeks-accelerator/oss/saas-starter-kit/internal/platform/web" "geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext" + "geeks-accelerator/oss/saas-starter-kit/internal/platform/web/weberror" _ "geeks-accelerator/oss/saas-starter-kit/internal/platform/web/weberror" + "geeks-accelerator/oss/saas-starter-kit/internal/project" _ "geeks-accelerator/oss/saas-starter-kit/internal/signup" "github.com/jmoiron/sqlx" + "github.com/pkg/errors" "gopkg.in/DataDog/dd-trace-go.v1/contrib/go-redis/redis" ) @@ -92,6 +96,8 @@ func API(shutdown chan os.Signal, log *log.Logger, env webcontext.Env, masterDB app.Handle("PATCH", "/v1/projects/archive", p.Archive, mid.AuthenticateHeader(authenticator), mid.HasRole(auth.RoleAdmin)) app.Handle("DELETE", "/v1/projects/:id", p.Delete, mid.AuthenticateHeader(authenticator), mid.HasRole(auth.RoleAdmin)) + app.Handle("GET", "/v1/examples/error-response", ExampleErrorResponse) + // Register swagger documentation. // TODO: Add authentication. Current authenticator requires an Authorization header // which breaks the browser experience. @@ -101,6 +107,36 @@ func API(shutdown chan os.Signal, log *log.Logger, env webcontext.Env, masterDB return app } +// ExampleErrorResponse returns example error messages. +func ExampleErrorResponse(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error { + v, err := webcontext.ContextValues(ctx) + if err != nil { + return err + } + + if qv := r.URL.Query().Get("test-validation-error"); qv != "" { + _, err := project.Create(ctx, auth.Claims{}, nil, project.ProjectCreateRequest{}, v.Now) + return web.RespondJsonError(ctx, w, err) + + } + + if qv := r.URL.Query().Get("test-web-error"); qv != "" { + terr := errors.New("Some random error") + terr = errors.WithMessage(terr, "Actual error message") + rerr := weberror.NewError(ctx, terr, http.StatusBadRequest).(*weberror.Error) + rerr.Message = "Test Web Error Message" + return web.RespondJsonError(ctx, w, rerr) + } + + if qv := r.URL.Query().Get("test-error"); qv != "" { + terr := errors.New("Test error") + terr = errors.WithMessage(terr, "Error message") + return web.RespondJsonError(ctx, w, terr) + } + + return nil +} + // Types godoc // @Summary List of types. // @Param data body weberror.FieldError false "Field Error" diff --git a/cmd/web-app/handlers/root.go b/cmd/web-app/handlers/root.go index 99d3d64..e4ea3e6 100644 --- a/cmd/web-app/handlers/root.go +++ b/cmd/web-app/handlers/root.go @@ -36,11 +36,7 @@ func (h *Root) Index(ctx context.Context, w http.ResponseWriter, r *http.Request // indexDashboard loads the dashboard for a user when they are authenticated. func (h *Root) indexDashboard(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error { - data := map[string]interface{}{ - "imgSizes": []int{100, 200, 300, 400, 500}, - } - - return h.Renderer.Render(ctx, w, r, TmplLayoutBase, "root-dashboard.gohtml", web.MIMETextHTMLCharsetUTF8, http.StatusOK, data) + return h.Renderer.Render(ctx, w, r, TmplLayoutBase, "root-dashboard.gohtml", web.MIMETextHTMLCharsetUTF8, http.StatusOK, nil) } // indexDefault loads the root index page when a user has no authentication. diff --git a/cmd/web-app/handlers/user.go b/cmd/web-app/handlers/user.go index 9817ac1..c91a3d3 100644 --- a/cmd/web-app/handlers/user.go +++ b/cmd/web-app/handlers/user.go @@ -22,7 +22,6 @@ import ( "github.com/gorilla/schema" "github.com/gorilla/sessions" "github.com/jmoiron/sqlx" - "github.com/pborman/uuid" "github.com/pkg/errors" ) @@ -824,10 +823,6 @@ func handleSessionToken(ctx context.Context, db *sqlx.DB, w http.ResponseWriter, sess := webcontext.ContextSession(ctx) - if sess.IsNew { - sess.ID = uuid.NewRandom().String() - } - sess.Options = &sessions.Options{ Path: "/", MaxAge: int(token.TTL.Seconds()), diff --git a/cmd/web-app/templates/content/user-view.gohtml b/cmd/web-app/templates/content/user-view.gohtml index 6f7d487..bc9ae4c 100644 --- a/cmd/web-app/templates/content/user-view.gohtml +++ b/cmd/web-app/templates/content/user-view.gohtml @@ -19,7 +19,7 @@
diff --git a/cmd/web-app/templates/layouts/base.gohtml b/cmd/web-app/templates/layouts/base.gohtml index df11402..b693d8d 100644 --- a/cmd/web-app/templates/layouts/base.gohtml +++ b/cmd/web-app/templates/layouts/base.gohtml @@ -125,13 +125,14 @@ {{ if or ($errMsg) ($errDetails) }}{{ $errDetails }}
{{ end }} diff --git a/cmd/web-app/tests/tests_test.go b/cmd/web-app/tests/tests_test.go deleted file mode 100644 index bf22894..0000000 --- a/cmd/web-app/tests/tests_test.go +++ /dev/null @@ -1,97 +0,0 @@ -package tests - -import ( - "crypto/rand" - "crypto/rsa" - "net/http" - "os" - "testing" - "time" - - "geeks-accelerator/oss/saas-starter-kit/cmd/web-app/handlers" - "geeks-accelerator/oss/saas-starter-kit/internal/platform/auth" - "geeks-accelerator/oss/saas-starter-kit/internal/platform/tests" - "geeks-accelerator/oss/saas-starter-kit/internal/user" -) - -var a http.Handler -var test *tests.Test - -// Information about the users we have created for testing. -var adminAuthorization string -var adminID string -var userAuthorization string -var userID string - -// TestMain is the entry point for testing. -func TestMain(m *testing.M) { - os.Exit(testMain(m)) -} - -func testMain(m *testing.M) int { - test = tests.New() - defer test.TearDown() - - // Create RSA keys to enable authentication in our service. - key, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - panic(err) - } - - kid := "4754d86b-7a6d-4df5-9c65-224741361492" - kf := auth.NewSingleKeyFunc(kid, key.Public().(*rsa.PublicKey)) - authenticator, err := auth.NewAuthenticator(key, kid, "RS256", kf) - if err != nil { - panic(err) - } - - shutdown := make(chan os.Signal, 1) - a = handlers.API(shutdown, test.Log, test.MasterDB, authenticator) - - // Create an admin user directly with our business logic. This creates an - // initial user that we will use for admin validated endpoints. - nu := user.NewUser{ - Email: "admin@ardanlabs.com", - Name: "Admin User", - Roles: []string{auth.RoleAdmin, auth.RoleUser}, - Password: "gophers", - PasswordConfirm: "gophers", - } - - admin, err := user.Create(tests.Context(), test.MasterDB, &nu, time.Now()) - if err != nil { - panic(err) - } - adminID = admin.ID.Hex() - - tkn, err := user.Authenticate(tests.Context(), test.MasterDB, authenticator, time.Now(), nu.Email, nu.Password) - if err != nil { - panic(err) - } - - adminAuthorization = "Bearer " + tkn.Token - - // Create a regular user to use when calling regular validated endpoints. - nu = user.NewUser{ - Email: "user@ardanlabs.com", - Name: "Regular User", - Roles: []string{auth.RoleUser}, - Password: "concurrency", - PasswordConfirm: "concurrency", - } - - usr, err := user.Create(tests.Context(), test.MasterDB, &nu, time.Now()) - if err != nil { - panic(err) - } - userID = usr.ID.Hex() - - tkn, err = user.Authenticate(tests.Context(), test.MasterDB, authenticator, time.Now(), nu.Email, nu.Password) - if err != nil { - panic(err) - } - - userAuthorization = "Bearer " + tkn.Token - - return m.Run() -} diff --git a/cmd/web-app/tests/user_test.go b/cmd/web-app/tests/user_test.go deleted file mode 100644 index 12f20b0..0000000 --- a/cmd/web-app/tests/user_test.go +++ /dev/null @@ -1,576 +0,0 @@ -package tests - -import ( - "bytes" - "encoding/json" - "net/http" - "net/http/httptest" - "strings" - "testing" - - "geeks-accelerator/oss/saas-starter-kit/internal/platform/auth" - "geeks-accelerator/oss/saas-starter-kit/internal/platform/tests" - "geeks-accelerator/oss/saas-starter-kit/internal/platform/web" - "geeks-accelerator/oss/saas-starter-kit/internal/user" - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" - "gopkg.in/mgo.v2/bson" -) - -// TestUsers is the entry point for testing user management functions. -func TestUsers(t *testing.T) { - defer tests.Recover(t) - - t.Run("getToken401", getToken401) - t.Run("getToken200", getToken200) - t.Run("postUser400", postUser400) - t.Run("postUser401", postUser401) - t.Run("postUser403", postUser403) - t.Run("getUser400", getUser400) - t.Run("getUser403", getUser403) - t.Run("getUser404", getUser404) - t.Run("deleteUser404", deleteUser404) - t.Run("putUser404", putUser404) - t.Run("crudUsers", crudUser) -} - -// getToken401 ensures an unknown user can't generate a token. -func getToken401(t *testing.T) { - r := httptest.NewRequest("GET", "/v1/users/token", nil) - w := httptest.NewRecorder() - - r.SetBasicAuth("unknown@example.com", "some-password") - - a.ServeHTTP(w, r) - - t.Log("Given the need to deny tokens to unknown users.") - { - t.Log("\tTest 0:\tWhen fetching a token with an unrecognized email.") - { - if w.Code != http.StatusUnauthorized { - t.Fatalf("\t%s\tShould receive a status code of 401 for the response : %v", tests.Failed, w.Code) - } - t.Logf("\t%s\tShould receive a status code of 401 for the response.", tests.Success) - } - } -} - -// getToken200 -func getToken200(t *testing.T) { - - r := httptest.NewRequest("GET", "/v1/users/token", nil) - w := httptest.NewRecorder() - - r.SetBasicAuth("admin@ardanlabs.com", "gophers") - - a.ServeHTTP(w, r) - - t.Log("Given the need to issues tokens to known users.") - { - t.Log("\tTest 0:\tWhen fetching a token with valid credentials.") - { - if w.Code != http.StatusOK { - t.Fatalf("\t%s\tShould receive a status code of 200 for the response : %v", tests.Failed, w.Code) - } - t.Logf("\t%s\tShould receive a status code of 200 for the response.", tests.Success) - - var got user.Token - if err := json.NewDecoder(w.Body).Decode(&got); err != nil { - t.Fatalf("\t%s\tShould be able to unmarshal the response : %v", tests.Failed, err) - } - t.Logf("\t%s\tShould be able to unmarshal the response.", tests.Success) - - // TODO(jlw) Should we ensure the token is valid? - } - } -} - -// postUser400 validates a user can't be created with the endpoint -// unless a valid user document is submitted. -func postUser400(t *testing.T) { - body, err := json.Marshal(&user.NewUser{}) - if err != nil { - t.Fatal(err) - } - - r := httptest.NewRequest("POST", "/v1/users", bytes.NewBuffer(body)) - w := httptest.NewRecorder() - - r.Header.Set("Authorization", adminAuthorization) - - a.ServeHTTP(w, r) - - t.Log("Given the need to validate a new user can't be created with an invalid document.") - { - t.Log("\tTest 0:\tWhen using an incomplete user value.") - { - if w.Code != http.StatusBadRequest { - t.Fatalf("\t%s\tShould receive a status code of 400 for the response : %v", tests.Failed, w.Code) - } - t.Logf("\t%s\tShould receive a status code of 400 for the response.", tests.Success) - - // Inspect the response. - var got web.ErrorResponse - if err := json.NewDecoder(w.Body).Decode(&got); err != nil { - t.Fatalf("\t%s\tShould be able to unmarshal the response to an error type : %v", tests.Failed, err) - } - t.Logf("\t%s\tShould be able to unmarshal the response to an error type.", tests.Success) - - // Define what we want to see. - want := web.ErrorResponse{ - Error: "field validation error", - Fields: []web.FieldError{ - {Field: "name", Error: "name is a required field"}, - {Field: "email", Error: "email is a required field"}, - {Field: "roles", Error: "roles is a required field"}, - {Field: "password", Error: "password is a required field"}, - }, - } - - // We can't rely on the order of the field errors so they have to be - // sorted. Tell the cmp package how to sort them. - sorter := cmpopts.SortSlices(func(a, b web.FieldError) bool { - return a.Field < b.Field - }) - - if diff := cmp.Diff(want, got, sorter); diff != "" { - t.Fatalf("\t%s\tShould get the expected result. Diff:\n%s", tests.Failed, diff) - } - t.Logf("\t%s\tShould get the expected result.", tests.Success) - } - } -} - -// postUser401 validates a user can't be created unless the calling user is -// authenticated. -func postUser401(t *testing.T) { - body, err := json.Marshal(&user.User{}) - if err != nil { - t.Fatal(err) - } - - r := httptest.NewRequest("POST", "/v1/users", bytes.NewBuffer(body)) - w := httptest.NewRecorder() - - r.Header.Set("Authorization", userAuthorization) - - a.ServeHTTP(w, r) - - t.Log("Given the need to validate a new user can't be created with an invalid document.") - { - t.Log("\tTest 0:\tWhen using an incomplete user value.") - { - if w.Code != http.StatusForbidden { - t.Fatalf("\t%s\tShould receive a status code of 403 for the response : %v", tests.Failed, w.Code) - } - t.Logf("\t%s\tShould receive a status code of 403 for the response.", tests.Success) - } - } -} - -// postUser403 validates a user can't be created unless the calling user is -// an admin user. Regular users can't do this. -func postUser403(t *testing.T) { - body, err := json.Marshal(&user.User{}) - if err != nil { - t.Fatal(err) - } - - r := httptest.NewRequest("POST", "/v1/users", bytes.NewBuffer(body)) - w := httptest.NewRecorder() - - // Not setting the Authorization header - - a.ServeHTTP(w, r) - - t.Log("Given the need to validate a new user can't be created with an invalid document.") - { - t.Log("\tTest 0:\tWhen using an incomplete user value.") - { - if w.Code != http.StatusUnauthorized { - t.Fatalf("\t%s\tShould receive a status code of 401 for the response : %v", tests.Failed, w.Code) - } - t.Logf("\t%s\tShould receive a status code of 401 for the response.", tests.Success) - } - } -} - -// getUser400 validates a user request for a malformed userid. -func getUser400(t *testing.T) { - id := "12345" - - r := httptest.NewRequest("GET", "/v1/users/"+id, nil) - w := httptest.NewRecorder() - - r.Header.Set("Authorization", adminAuthorization) - - a.ServeHTTP(w, r) - - t.Log("Given the need to validate getting a user with a malformed userid.") - { - t.Logf("\tTest 0:\tWhen using the new user %s.", id) - { - if w.Code != http.StatusBadRequest { - t.Fatalf("\t%s\tShould receive a status code of 400 for the response : %v", tests.Failed, w.Code) - } - t.Logf("\t%s\tShould receive a status code of 400 for the response.", tests.Success) - - recv := w.Body.String() - resp := `{"error":"ID is not in its proper form"}` - if resp != recv { - t.Log("Got :", recv) - t.Log("Want:", resp) - t.Fatalf("\t%s\tShould get the expected result.", tests.Failed) - } - t.Logf("\t%s\tShould get the expected result.", tests.Success) - } - } -} - -// getUser403 validates a regular user can't fetch anyone but themselves -func getUser403(t *testing.T) { - t.Log("Given the need to validate regular users can't fetch other users.") - { - t.Logf("\tTest 0:\tWhen fetching the admin user as a regular user.") - { - r := httptest.NewRequest("GET", "/v1/users/"+adminID, nil) - w := httptest.NewRecorder() - - r.Header.Set("Authorization", userAuthorization) - - a.ServeHTTP(w, r) - - if w.Code != http.StatusForbidden { - t.Fatalf("\t%s\tShould receive a status code of 403 for the response : %v", tests.Failed, w.Code) - } - t.Logf("\t%s\tShould receive a status code of 403 for the response.", tests.Success) - - recv := w.Body.String() - resp := `{"error":"Attempted action is not allowed"}` - if resp != recv { - t.Log("Got :", recv) - t.Log("Want:", resp) - t.Fatalf("\t%s\tShould get the expected result.", tests.Failed) - } - t.Logf("\t%s\tShould get the expected result.", tests.Success) - } - - t.Logf("\tTest 1:\tWhen fetching the user as a themselves.") - { - - r := httptest.NewRequest("GET", "/v1/users/"+userID, nil) - w := httptest.NewRecorder() - - r.Header.Set("Authorization", userAuthorization) - - a.ServeHTTP(w, r) - if w.Code != http.StatusOK { - t.Fatalf("\t%s\tShould receive a status code of 200 for the response : %v", tests.Failed, w.Code) - } - t.Logf("\t%s\tShould receive a status code of 200 for the response.", tests.Success) - } - } -} - -// getUser404 validates a user request for a user that does not exist with the endpoint. -func getUser404(t *testing.T) { - id := bson.NewObjectId().Hex() - - r := httptest.NewRequest("GET", "/v1/users/"+id, nil) - w := httptest.NewRecorder() - - r.Header.Set("Authorization", adminAuthorization) - - a.ServeHTTP(w, r) - - t.Log("Given the need to validate getting a user with an unknown id.") - { - t.Logf("\tTest 0:\tWhen using the new user %s.", id) - { - if w.Code != http.StatusNotFound { - t.Fatalf("\t%s\tShould receive a status code of 404 for the response : %v", tests.Failed, w.Code) - } - t.Logf("\t%s\tShould receive a status code of 404 for the response.", tests.Success) - - recv := w.Body.String() - resp := "Entity not found" - if !strings.Contains(recv, resp) { - t.Log("Got :", recv) - t.Log("Want:", resp) - t.Fatalf("\t%s\tShould get the expected result.", tests.Failed) - } - t.Logf("\t%s\tShould get the expected result.", tests.Success) - } - } -} - -// deleteUser404 validates deleting a user that does not exist. -func deleteUser404(t *testing.T) { - id := bson.NewObjectId().Hex() - - r := httptest.NewRequest("DELETE", "/v1/users/"+id, nil) - w := httptest.NewRecorder() - - r.Header.Set("Authorization", adminAuthorization) - - a.ServeHTTP(w, r) - - t.Log("Given the need to validate deleting a user that does not exist.") - { - t.Logf("\tTest 0:\tWhen using the new user %s.", id) - { - if w.Code != http.StatusNotFound { - t.Fatalf("\t%s\tShould receive a status code of 404 for the response : %v", tests.Failed, w.Code) - } - t.Logf("\t%s\tShould receive a status code of 404 for the response.", tests.Success) - - recv := w.Body.String() - resp := "Entity not found" - if !strings.Contains(recv, resp) { - t.Log("Got :", recv) - t.Log("Want:", resp) - t.Fatalf("\t%s\tShould get the expected result.", tests.Failed) - } - t.Logf("\t%s\tShould get the expected result.", tests.Success) - } - } -} - -// putUser404 validates updating a user that does not exist. -func putUser404(t *testing.T) { - u := user.UpdateUser{ - Name: tests.StringPointer("Doesn't Exist"), - } - - id := bson.NewObjectId().Hex() - - body, err := json.Marshal(&u) - if err != nil { - t.Fatal(err) - } - - r := httptest.NewRequest("PUT", "/v1/users/"+id, bytes.NewBuffer(body)) - w := httptest.NewRecorder() - - r.Header.Set("Authorization", adminAuthorization) - - a.ServeHTTP(w, r) - - t.Log("Given the need to validate updating a user that does not exist.") - { - t.Logf("\tTest 0:\tWhen using the new user %s.", id) - { - if w.Code != http.StatusNotFound { - t.Fatalf("\t%s\tShould receive a status code of 404 for the response : %v", tests.Failed, w.Code) - } - t.Logf("\t%s\tShould receive a status code of 404 for the response.", tests.Success) - - recv := w.Body.String() - resp := "Entity not found" - if !strings.Contains(recv, resp) { - t.Log("Got :", recv) - t.Log("Want:", resp) - t.Fatalf("\t%s\tShould get the expected result.", tests.Failed) - } - t.Logf("\t%s\tShould get the expected result.", tests.Success) - } - } -} - -// crudUser performs a complete test of CRUD against the api. -func crudUser(t *testing.T) { - nu := postUser201(t) - defer deleteUser204(t, nu.ID.Hex()) - - getUser200(t, nu.ID.Hex()) - putUser204(t, nu.ID.Hex()) - putUser403(t, nu.ID.Hex()) -} - -// postUser201 validates a user can be created with the endpoint. -func postUser201(t *testing.T) user.User { - nu := user.NewUser{ - Name: "Bill Kennedy", - Email: "bill@ardanlabs.com", - Roles: []string{auth.RoleAdmin}, - Password: "gophers", - PasswordConfirm: "gophers", - } - - body, err := json.Marshal(&nu) - if err != nil { - t.Fatal(err) - } - - r := httptest.NewRequest("POST", "/v1/users", bytes.NewBuffer(body)) - w := httptest.NewRecorder() - - r.Header.Set("Authorization", adminAuthorization) - - a.ServeHTTP(w, r) - - // u is the value we will return. - var u user.User - - t.Log("Given the need to create a new user with the users endpoint.") - { - t.Log("\tTest 0:\tWhen using the declared user value.") - { - if w.Code != http.StatusCreated { - t.Fatalf("\t%s\tShould receive a status code of 201 for the response : %v", tests.Failed, w.Code) - } - t.Logf("\t%s\tShould receive a status code of 201 for the response.", tests.Success) - - if err := json.NewDecoder(w.Body).Decode(&u); err != nil { - t.Fatalf("\t%s\tShould be able to unmarshal the response : %v", tests.Failed, err) - } - - // Define what we wanted to receive. We will just trust the generated - // fields like ID and Dates so we copy u. - want := u - want.Name = "Bill Kennedy" - want.Email = "bill@ardanlabs.com" - want.Roles = []string{auth.RoleAdmin} - - if diff := cmp.Diff(want, u); diff != "" { - t.Fatalf("\t%s\tShould get the expected result. Diff:\n%s", tests.Failed, diff) - } - t.Logf("\t%s\tShould get the expected result.", tests.Success) - } - } - - return u -} - -// deleteUser200 validates deleting a user that does exist. -func deleteUser204(t *testing.T, id string) { - r := httptest.NewRequest("DELETE", "/v1/users/"+id, nil) - w := httptest.NewRecorder() - - r.Header.Set("Authorization", adminAuthorization) - - a.ServeHTTP(w, r) - - t.Log("Given the need to validate deleting a user that does exist.") - { - t.Logf("\tTest 0:\tWhen using the new user %s.", id) - { - if w.Code != http.StatusNoContent { - t.Fatalf("\t%s\tShould receive a status code of 204 for the response : %v", tests.Failed, w.Code) - } - t.Logf("\t%s\tShould receive a status code of 204 for the response.", tests.Success) - } - } -} - -// getUser200 validates a user request for an existing userid. -func getUser200(t *testing.T, id string) { - r := httptest.NewRequest("GET", "/v1/users/"+id, nil) - w := httptest.NewRecorder() - - r.Header.Set("Authorization", adminAuthorization) - - a.ServeHTTP(w, r) - - t.Log("Given the need to validate getting a user that exsits.") - { - t.Logf("\tTest 0:\tWhen using the new user %s.", id) - { - if w.Code != http.StatusOK { - t.Fatalf("\t%s\tShould receive a status code of 200 for the response : %v", tests.Failed, w.Code) - } - t.Logf("\t%s\tShould receive a status code of 200 for the response.", tests.Success) - - var u user.User - if err := json.NewDecoder(w.Body).Decode(&u); err != nil { - t.Fatalf("\t%s\tShould be able to unmarshal the response : %v", tests.Failed, err) - } - - // Define what we wanted to receive. We will just trust the generated - // fields like Dates so we copy p. - want := u - want.ID = bson.ObjectIdHex(id) - want.Name = "Bill Kennedy" - want.Email = "bill@ardanlabs.com" - want.Roles = []string{auth.RoleAdmin} - - if diff := cmp.Diff(want, u); diff != "" { - t.Fatalf("\t%s\tShould get the expected result. Diff:\n%s", tests.Failed, diff) - } - t.Logf("\t%s\tShould get the expected result.", tests.Success) - } - } -} - -// putUser204 validates updating a user that does exist. -func putUser204(t *testing.T, id string) { - body := `{"name": "Jacob Walker"}` - - r := httptest.NewRequest("PUT", "/v1/users/"+id, strings.NewReader(body)) - w := httptest.NewRecorder() - - r.Header.Set("Authorization", adminAuthorization) - - a.ServeHTTP(w, r) - - t.Log("Given the need to update a user with the users endpoint.") - { - t.Log("\tTest 0:\tWhen using the modified user value.") - { - if w.Code != http.StatusNoContent { - t.Fatalf("\t%s\tShould receive a status code of 204 for the response : %v", tests.Failed, w.Code) - } - t.Logf("\t%s\tShould receive a status code of 204 for the response.", tests.Success) - - r = httptest.NewRequest("GET", "/v1/users/"+id, nil) - w = httptest.NewRecorder() - - r.Header.Set("Authorization", adminAuthorization) - - a.ServeHTTP(w, r) - - if w.Code != http.StatusOK { - t.Fatalf("\t%s\tShould receive a status code of 200 for the retrieve : %v", tests.Failed, w.Code) - } - t.Logf("\t%s\tShould receive a status code of 200 for the retrieve.", tests.Success) - - var ru user.User - if err := json.NewDecoder(w.Body).Decode(&ru); err != nil { - t.Fatalf("\t%s\tShould be able to unmarshal the response : %v", tests.Failed, err) - } - - if ru.Name != "Jacob Walker" { - t.Fatalf("\t%s\tShould see an updated Name : got %q want %q", tests.Failed, ru.Name, "Jacob Walker") - } - t.Logf("\t%s\tShould see an updated Name.", tests.Success) - - if ru.Email != "bill@ardanlabs.com" { - t.Fatalf("\t%s\tShould not affect other fields like Email : got %q want %q", tests.Failed, ru.Email, "bill@ardanlabs.com") - } - t.Logf("\t%s\tShould not affect other fields like Email.", tests.Success) - } - } -} - -// putUser403 validates that a user can't modify users unless they are an admin. -func putUser403(t *testing.T, id string) { - body := `{"name": "Anna Walker"}` - - r := httptest.NewRequest("PUT", "/v1/users/"+id, strings.NewReader(body)) - w := httptest.NewRecorder() - - r.Header.Set("Authorization", userAuthorization) - - a.ServeHTTP(w, r) - - t.Log("Given the need to update a user with the users endpoint.") - { - t.Log("\tTest 0:\tWhen a non-admin user makes a request") - { - if w.Code != http.StatusForbidden { - t.Fatalf("\t%s\tShould receive a status code of 403 for the response : %v", tests.Failed, w.Code) - } - t.Logf("\t%s\tShould receive a status code of 403 for the response.", tests.Success) - } - } -} diff --git a/internal/platform/web/template-renderer/template_renderer.go b/internal/platform/web/template-renderer/template_renderer.go index 46b3a58..a2b0bf5 100644 --- a/internal/platform/web/template-renderer/template_renderer.go +++ b/internal/platform/web/template-renderer/template_renderer.go @@ -135,7 +135,16 @@ func NewTemplate(templateFuncs template.FuncMap) *Template { } return false }, - + "HasField": func(v interface{}, name string) bool { + rv := reflect.ValueOf(v) + if rv.Kind() == reflect.Ptr { + rv = rv.Elem() + } + if rv.Kind() != reflect.Struct { + return false + } + return rv.FieldByName(name).IsValid() + }, "dict": func(values ...interface{}) (map[string]interface{}, error) { if len(values) == 0 { return nil, errors.New("invalid dict call") diff --git a/internal/platform/web/webcontext/session.go b/internal/platform/web/webcontext/session.go index 7c4e498..67654ad 100644 --- a/internal/platform/web/webcontext/session.go +++ b/internal/platform/web/webcontext/session.go @@ -2,7 +2,9 @@ package webcontext import ( "context" + "github.com/gorilla/sessions" + "github.com/pborman/uuid" ) // ctxKeySession represents the type of value for the context key. @@ -16,6 +18,9 @@ const ( SessionKeyAccessToken = iota ) +// KeySessionID is the key used to store the ID of the session in its values. +const KeySessionID = "_sid" + // ContextWithSession appends a universal translator to a context. func ContextWithSession(ctx context.Context, session *sessions.Session) context.Context { return context.WithValue(ctx, KeySession, session) @@ -24,11 +29,16 @@ func ContextWithSession(ctx context.Context, session *sessions.Session) context. // ContextSession returns the session from a context. func ContextSession(ctx context.Context) *sessions.Session { if s, ok := ctx.Value(KeySession).(*sessions.Session); ok { + if sid, ok := s.Values[KeySessionID].(string); ok { + s.ID = sid + } + return s } return nil } +// ContextAccessToken returns the JWT access token from the context session. func ContextAccessToken(ctx context.Context) (string, bool) { sess := ContextSession(ctx) if sess == nil { @@ -40,18 +50,28 @@ func ContextAccessToken(ctx context.Context) (string, bool) { return "", false } +// SessionInit creates a new session with a valid JWT access token. func SessionInit(session *sessions.Session, accessToken string) *sessions.Session { + // Always create a new session ID to ensure when session ID is being used as a cache key, logout/login + // forces any cache to be flushed. + session.ID = uuid.NewRandom().String() + + // Not sure why sessions.Session has the ID prop but it is not persisted by default. + session.Values[KeySessionID] = session.ID + session.Values[SessionKeyAccessToken] = accessToken return session } +// SessionUpdateAccessToken updates the JWT access token stored in the session. func SessionUpdateAccessToken(session *sessions.Session, accessToken string) *sessions.Session { session.Values[SessionKeyAccessToken] = accessToken return session } +// SessionDestroy removes the access token from the session which revokes authentication for the user. func SessionDestroy(session *sessions.Session) *sessions.Session { delete(session.Values, SessionKeyAccessToken)