Commit bf1ec63d authored by Lee Brown's avatar Lee Brown

Added api example error responses and some minor bug fixes

parent e8f0f68d
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"
......
......@@ -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.
......
......@@ -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()),
......
......@@ -19,7 +19,7 @@
</a>
<div class="dropdown-menu dropdown-menu-right shadow animated--fade-in" aria-labelledby="dropdownMenuLink" x-placement="bottom-end" style="position: absolute; transform: translate3d(-156px, 19px, 0px); top: 0px; left: 0px; will-change: transform;">
<div class="dropdown-header">Actions</div>
<a class="dropdown-item" href="/account/update">Update Details</a>
<a class="dropdown-item" href="/user/update">Update Details</a>
<a class="dropdown-item" href="https://gravatar.com" target="_blank">Update Avatar</a>
</div>
</div>
......
......@@ -125,13 +125,14 @@
{{ if or ($errMsg) ($errDetails) }}
<div class="alert alert-danger" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"> <span aria-hidden="true">×</span> </button> {{ if $errMsg }}<h3>{{ $errMsg }}</h3> {{end}}
{{ if .error.Fields }}
{{ if HasField .error "Fields" }}
<ul>
{{ range $i := .error.Fields }}
<li>{{ if $i.Display }}{{ $i.Display }}{{ else }}{{ $i.Error }}{{ end }}</li>
{{end}}
</ul>
{{ end }}
{{ if $errDetails }}
<p><small>{{ $errDetails }}</small></p>
{{ end }}
......
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()
}
This diff is collapsed.
......@@ -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")
......
......@@ -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)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment