diff options
author | Michael Muré <batolettre@gmail.com> | 2020-06-21 22:12:04 +0200 |
---|---|---|
committer | Michael Muré <batolettre@gmail.com> | 2020-06-27 23:03:05 +0200 |
commit | 2ab6381a94d55fa22b80acdbb18849d6b24951f9 (patch) | |
tree | 99942b000955623ea7466b9fa4cc7dab37645df6 | |
parent | 5f72b04ef8e84b1c367ca6874519706318e351f5 (diff) | |
download | git-bug-2ab6381a94d55fa22b80acdbb18849d6b24951f9.tar.gz |
Reorganize the webUI and API code
Included in the changes:
- create a new /api root package to hold all API code, migrate /graphql in there
- git API handlers all use the cache instead of the repo directly
- git API handlers are now tested
- git API handlers now require a "repo" mux parameter
- lots of untangling of API/handlers/middleware
- less code in commands/webui.go
-rw-r--r-- | README.md | 2 | ||||
-rw-r--r-- | api/auth/context.go | 28 | ||||
-rw-r--r-- | api/auth/errors.go (renamed from graphql/graphqlidentity/errors.go) | 2 | ||||
-rw-r--r-- | api/auth/middleware.go | 16 | ||||
-rw-r--r-- | api/graphql/connections/connection_template.go (renamed from graphql/connections/connection_template.go) | 2 | ||||
-rw-r--r-- | api/graphql/connections/connections.go (renamed from graphql/connections/connections.go) | 0 | ||||
-rw-r--r-- | api/graphql/connections/edges.go (renamed from graphql/connections/edges.go) | 0 | ||||
-rw-r--r-- | api/graphql/connections/gen_comment.go (renamed from graphql/connections/gen_comment.go) | 2 | ||||
-rw-r--r-- | api/graphql/connections/gen_identity.go (renamed from graphql/connections/gen_identity.go) | 2 | ||||
-rw-r--r-- | api/graphql/connections/gen_label.go (renamed from graphql/connections/gen_label.go) | 2 | ||||
-rw-r--r-- | api/graphql/connections/gen_lazy_bug.go (renamed from graphql/connections/gen_lazy_bug.go) | 2 | ||||
-rw-r--r-- | api/graphql/connections/gen_lazy_identity.go (renamed from graphql/connections/gen_lazy_identity.go) | 2 | ||||
-rw-r--r-- | api/graphql/connections/gen_operation.go (renamed from graphql/connections/gen_operation.go) | 2 | ||||
-rw-r--r-- | api/graphql/connections/gen_timeline.go (renamed from graphql/connections/gen_timeline.go) | 2 | ||||
-rw-r--r-- | api/graphql/gen_graphql.go (renamed from graphql/gen_graphql.go) | 0 | ||||
-rw-r--r-- | api/graphql/gqlgen.yml (renamed from graphql/gqlgen.yml) | 0 | ||||
-rw-r--r-- | api/graphql/graph/gen_graph.go (renamed from graphql/graph/gen_graph.go) | 7 | ||||
-rw-r--r-- | api/graphql/graphql_test.go (renamed from graphql/graphql_test.go) | 17 | ||||
-rw-r--r-- | api/graphql/handler.go | 32 | ||||
-rw-r--r-- | api/graphql/models/edges.go (renamed from graphql/models/edges.go) | 0 | ||||
-rw-r--r-- | api/graphql/models/gen_models.go (renamed from graphql/models/gen_models.go) | 0 | ||||
-rw-r--r-- | api/graphql/models/lazy_bug.go (renamed from graphql/models/lazy_bug.go) | 0 | ||||
-rw-r--r-- | api/graphql/models/lazy_identity.go (renamed from graphql/models/lazy_identity.go) | 0 | ||||
-rw-r--r-- | api/graphql/models/models.go (renamed from graphql/models/models.go) | 0 | ||||
-rw-r--r-- | api/graphql/resolvers/bug.go (renamed from graphql/resolvers/bug.go) | 6 | ||||
-rw-r--r-- | api/graphql/resolvers/color.go (renamed from graphql/resolvers/color.go) | 2 | ||||
-rw-r--r-- | api/graphql/resolvers/comment.go (renamed from graphql/resolvers/comment.go) | 4 | ||||
-rw-r--r-- | api/graphql/resolvers/identity.go (renamed from graphql/resolvers/identity.go) | 4 | ||||
-rw-r--r-- | api/graphql/resolvers/label.go (renamed from graphql/resolvers/label.go) | 4 | ||||
-rw-r--r-- | api/graphql/resolvers/mutation.go (renamed from graphql/resolvers/mutation.go) | 34 | ||||
-rw-r--r-- | api/graphql/resolvers/operations.go (renamed from graphql/resolvers/operations.go) | 4 | ||||
-rw-r--r-- | api/graphql/resolvers/query.go (renamed from graphql/resolvers/query.go) | 4 | ||||
-rw-r--r-- | api/graphql/resolvers/repo.go (renamed from graphql/resolvers/repo.go) | 12 | ||||
-rw-r--r-- | api/graphql/resolvers/root.go (renamed from graphql/resolvers/root.go) | 12 | ||||
-rw-r--r-- | api/graphql/resolvers/timeline.go (renamed from graphql/resolvers/timeline.go) | 4 | ||||
-rw-r--r-- | api/graphql/schema/bug.graphql (renamed from graphql/schema/bug.graphql) | 0 | ||||
-rw-r--r-- | api/graphql/schema/identity.graphql (renamed from graphql/schema/identity.graphql) | 0 | ||||
-rw-r--r-- | api/graphql/schema/label.graphql (renamed from graphql/schema/label.graphql) | 0 | ||||
-rw-r--r-- | api/graphql/schema/mutations.graphql (renamed from graphql/schema/mutations.graphql) | 0 | ||||
-rw-r--r-- | api/graphql/schema/operations.graphql (renamed from graphql/schema/operations.graphql) | 0 | ||||
-rw-r--r-- | api/graphql/schema/repository.graphql (renamed from graphql/schema/repository.graphql) | 0 | ||||
-rw-r--r-- | api/graphql/schema/root.graphql (renamed from graphql/schema/root.graphql) | 0 | ||||
-rw-r--r-- | api/graphql/schema/timeline.graphql (renamed from graphql/schema/timeline.graphql) | 0 | ||||
-rw-r--r-- | api/graphql/schema/types.graphql (renamed from graphql/schema/types.graphql) | 0 | ||||
-rw-r--r-- | api/http/git_file_handler.go | 61 | ||||
-rw-r--r-- | api/http/git_file_handlers_test.go | 91 | ||||
-rw-r--r-- | api/http/git_file_upload_handler.go | 104 | ||||
-rw-r--r-- | cache/multi_repo_cache.go | 16 | ||||
-rw-r--r-- | cache/repo_cache.go | 10 | ||||
-rw-r--r-- | commands/webui.go | 176 | ||||
-rw-r--r-- | graphql/graphqlidentity/graphqlidentity.go | 41 | ||||
-rw-r--r-- | graphql/handler.go | 39 | ||||
-rw-r--r-- | webui/codegen.yaml | 2 | ||||
-rw-r--r-- | webui/handler.go | 31 |
54 files changed, 472 insertions, 309 deletions
@@ -174,7 +174,7 @@ You can launch a rich Web UI with `git bug webui`. This web UI is entirely packed inside the same go binary and serve static content through a localhost http server. -The web UI interact with the backend through a GraphQL API. The schema is available [here](graphql/). +The web UI interact with the backend through a GraphQL API. The schema is available [here](api/graphql/schema). ## Bridges diff --git a/api/auth/context.go b/api/auth/context.go new file mode 100644 index 00000000..17171261 --- /dev/null +++ b/api/auth/context.go @@ -0,0 +1,28 @@ +// Package auth contains helpers for managing identities within the GraphQL API. +package auth + +import ( + "context" + + "github.com/MichaelMure/git-bug/cache" + "github.com/MichaelMure/git-bug/entity" +) + +// identityCtxKey is a unique context key, accessible only in this package. +var identityCtxKey = &struct{}{} + +// CtxWithUser attaches an Identity to a context. +func CtxWithUser(ctx context.Context, userId entity.Id) context.Context { + return context.WithValue(ctx, identityCtxKey, userId) +} + +// UserFromCtx retrieves an IdentityCache from the context. +// If there is no identity in the context, ErrNotAuthenticated is returned. +// If an error occurs while resolving the identity (e.g. I/O error), then it will be returned. +func UserFromCtx(ctx context.Context, r *cache.RepoCache) (*cache.IdentityCache, error) { + id, ok := ctx.Value(identityCtxKey).(entity.Id) + if !ok { + return nil, ErrNotAuthenticated + } + return r.ResolveIdentity(id) +} diff --git a/graphql/graphqlidentity/errors.go b/api/auth/errors.go index 5ec58b32..9675afbf 100644 --- a/graphql/graphqlidentity/errors.go +++ b/api/auth/errors.go @@ -1,4 +1,4 @@ -package graphqlidentity +package auth import "errors" diff --git a/api/auth/middleware.go b/api/auth/middleware.go new file mode 100644 index 00000000..d1d654ce --- /dev/null +++ b/api/auth/middleware.go @@ -0,0 +1,16 @@ +package auth + +import ( + "net/http" + + "github.com/MichaelMure/git-bug/entity" +) + +func Middleware(fixedUserId entity.Id) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := CtxWithUser(r.Context(), fixedUserId) + next.ServeHTTP(w, r.WithContext(ctx)) + }) + } +} diff --git a/graphql/connections/connection_template.go b/api/graphql/connections/connection_template.go index f276b2d0..935a0a77 100644 --- a/graphql/connections/connection_template.go +++ b/api/graphql/connections/connection_template.go @@ -5,7 +5,7 @@ import ( "github.com/cheekybits/genny/generic" - "github.com/MichaelMure/git-bug/graphql/models" + "github.com/MichaelMure/git-bug/api/graphql/models" ) // Name define the name of the connection diff --git a/graphql/connections/connections.go b/api/graphql/connections/connections.go index 0083f8b2..0083f8b2 100644 --- a/graphql/connections/connections.go +++ b/api/graphql/connections/connections.go diff --git a/graphql/connections/edges.go b/api/graphql/connections/edges.go index 4e37fcd9..4e37fcd9 100644 --- a/graphql/connections/edges.go +++ b/api/graphql/connections/edges.go diff --git a/graphql/connections/gen_comment.go b/api/graphql/connections/gen_comment.go index 9f96f2bb..bae74030 100644 --- a/graphql/connections/gen_comment.go +++ b/api/graphql/connections/gen_comment.go @@ -7,8 +7,8 @@ package connections import ( "fmt" + "github.com/MichaelMure/git-bug/api/graphql/models" "github.com/MichaelMure/git-bug/bug" - "github.com/MichaelMure/git-bug/graphql/models" ) // BugCommentEdgeMaker define a function that take a bug.Comment and an offset and diff --git a/graphql/connections/gen_identity.go b/api/graphql/connections/gen_identity.go index 061e8936..2138b17e 100644 --- a/graphql/connections/gen_identity.go +++ b/api/graphql/connections/gen_identity.go @@ -7,7 +7,7 @@ package connections import ( "fmt" - "github.com/MichaelMure/git-bug/graphql/models" + "github.com/MichaelMure/git-bug/api/graphql/models" ) // ModelsIdentityWrapperEdgeMaker define a function that take a models.IdentityWrapper and an offset and diff --git a/graphql/connections/gen_label.go b/api/graphql/connections/gen_label.go index 7f1b2fc9..39b1c536 100644 --- a/graphql/connections/gen_label.go +++ b/api/graphql/connections/gen_label.go @@ -7,8 +7,8 @@ package connections import ( "fmt" + "github.com/MichaelMure/git-bug/api/graphql/models" "github.com/MichaelMure/git-bug/bug" - "github.com/MichaelMure/git-bug/graphql/models" ) // BugLabelEdgeMaker define a function that take a bug.Label and an offset and diff --git a/graphql/connections/gen_lazy_bug.go b/api/graphql/connections/gen_lazy_bug.go index 9638e86b..1dc4692e 100644 --- a/graphql/connections/gen_lazy_bug.go +++ b/api/graphql/connections/gen_lazy_bug.go @@ -7,8 +7,8 @@ package connections import ( "fmt" + "github.com/MichaelMure/git-bug/api/graphql/models" "github.com/MichaelMure/git-bug/entity" - "github.com/MichaelMure/git-bug/graphql/models" ) // EntityIdEdgeMaker define a function that take a entity.Id and an offset and diff --git a/graphql/connections/gen_lazy_identity.go b/api/graphql/connections/gen_lazy_identity.go index 932d802c..4996e219 100644 --- a/graphql/connections/gen_lazy_identity.go +++ b/api/graphql/connections/gen_lazy_identity.go @@ -7,8 +7,8 @@ package connections import ( "fmt" + "github.com/MichaelMure/git-bug/api/graphql/models" "github.com/MichaelMure/git-bug/entity" - "github.com/MichaelMure/git-bug/graphql/models" ) // EntityIdEdgeMaker define a function that take a entity.Id and an offset and diff --git a/graphql/connections/gen_operation.go b/api/graphql/connections/gen_operation.go index 0f40e2c4..4bd84895 100644 --- a/graphql/connections/gen_operation.go +++ b/api/graphql/connections/gen_operation.go @@ -7,8 +7,8 @@ package connections import ( "fmt" + "github.com/MichaelMure/git-bug/api/graphql/models" "github.com/MichaelMure/git-bug/bug" - "github.com/MichaelMure/git-bug/graphql/models" ) // BugOperationEdgeMaker define a function that take a bug.Operation and an offset and diff --git a/graphql/connections/gen_timeline.go b/api/graphql/connections/gen_timeline.go index 01dac96b..952d095c 100644 --- a/graphql/connections/gen_timeline.go +++ b/api/graphql/connections/gen_timeline.go @@ -7,8 +7,8 @@ package connections import ( "fmt" + "github.com/MichaelMure/git-bug/api/graphql/models" "github.com/MichaelMure/git-bug/bug" - "github.com/MichaelMure/git-bug/graphql/models" ) // BugTimelineItemEdgeMaker define a function that take a bug.TimelineItem and an offset and diff --git a/graphql/gen_graphql.go b/api/graphql/gen_graphql.go index 47f2c458..47f2c458 100644 --- a/graphql/gen_graphql.go +++ b/api/graphql/gen_graphql.go diff --git a/graphql/gqlgen.yml b/api/graphql/gqlgen.yml index fe33b8c7..fe33b8c7 100644 --- a/graphql/gqlgen.yml +++ b/api/graphql/gqlgen.yml diff --git a/graphql/graph/gen_graph.go b/api/graphql/graph/gen_graph.go index 277cb7cb..be0e92ee 100644 --- a/graphql/graph/gen_graph.go +++ b/api/graphql/graph/gen_graph.go @@ -15,11 +15,12 @@ import ( "github.com/99designs/gqlgen/graphql" "github.com/99designs/gqlgen/graphql/introspection" - "github.com/MichaelMure/git-bug/bug" - "github.com/MichaelMure/git-bug/graphql/models" - "github.com/MichaelMure/git-bug/util/git" "github.com/vektah/gqlparser" "github.com/vektah/gqlparser/ast" + + "github.com/MichaelMure/git-bug/api/graphql/models" + "github.com/MichaelMure/git-bug/bug" + "github.com/MichaelMure/git-bug/util/git" ) // region ************************** generated!.gotpl ************************** diff --git a/graphql/graphql_test.go b/api/graphql/graphql_test.go index 0ff2c3fb..45e88e9a 100644 --- a/graphql/graphql_test.go +++ b/api/graphql/graphql_test.go @@ -4,8 +4,11 @@ import ( "testing" "github.com/99designs/gqlgen/client" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" - "github.com/MichaelMure/git-bug/graphql/models" + "github.com/MichaelMure/git-bug/api/graphql/models" + "github.com/MichaelMure/git-bug/cache" "github.com/MichaelMure/git-bug/misc/random_bugs" "github.com/MichaelMure/git-bug/repository" ) @@ -16,10 +19,11 @@ func TestQueries(t *testing.T) { random_bugs.FillRepoWithSeed(repo, 10, 42) - handler, err := NewHandler(repo) - if err != nil { - t.Fatal(err) - } + mrc := cache.NewMultiRepoCache() + _, err := mrc.RegisterDefaultRepository(repo) + require.NoError(t, err) + + handler := NewHandler(mrc) c := client.New(handler) @@ -211,5 +215,6 @@ func TestQueries(t *testing.T) { } } - c.MustPost(query, &resp) + err = c.Post(query, &resp) + assert.NoError(t, err) } diff --git a/api/graphql/handler.go b/api/graphql/handler.go new file mode 100644 index 00000000..03dc32e9 --- /dev/null +++ b/api/graphql/handler.go @@ -0,0 +1,32 @@ +//go:generate go run gen_graphql.go + +// Package graphql contains the root GraphQL http handler +package graphql + +import ( + "io" + "net/http" + + "github.com/99designs/gqlgen/graphql/handler" + + "github.com/MichaelMure/git-bug/api/graphql/graph" + "github.com/MichaelMure/git-bug/api/graphql/resolvers" + "github.com/MichaelMure/git-bug/cache" +) + +// Handler is the root GraphQL http handler +type Handler struct { + http.Handler + io.Closer +} + +func NewHandler(mrc *cache.MultiRepoCache) Handler { + rootResolver := resolvers.NewRootResolver(mrc) + config := graph.Config{Resolvers: rootResolver} + h := handler.NewDefaultServer(graph.NewExecutableSchema(config)) + + return Handler{ + Handler: h, + Closer: rootResolver, + } +} diff --git a/graphql/models/edges.go b/api/graphql/models/edges.go index 6a331e3e..6a331e3e 100644 --- a/graphql/models/edges.go +++ b/api/graphql/models/edges.go diff --git a/graphql/models/gen_models.go b/api/graphql/models/gen_models.go index cbece6fe..cbece6fe 100644 --- a/graphql/models/gen_models.go +++ b/api/graphql/models/gen_models.go diff --git a/graphql/models/lazy_bug.go b/api/graphql/models/lazy_bug.go index a7840df2..a7840df2 100644 --- a/graphql/models/lazy_bug.go +++ b/api/graphql/models/lazy_bug.go diff --git a/graphql/models/lazy_identity.go b/api/graphql/models/lazy_identity.go index 344bb5f0..344bb5f0 100644 --- a/graphql/models/lazy_identity.go +++ b/api/graphql/models/lazy_identity.go diff --git a/graphql/models/models.go b/api/graphql/models/models.go index 816a04a8..816a04a8 100644 --- a/graphql/models/models.go +++ b/api/graphql/models/models.go diff --git a/graphql/resolvers/bug.go b/api/graphql/resolvers/bug.go index fd8f4b6e..815cba8d 100644 --- a/graphql/resolvers/bug.go +++ b/api/graphql/resolvers/bug.go @@ -3,10 +3,10 @@ package resolvers import ( "context" + "github.com/MichaelMure/git-bug/api/graphql/connections" + "github.com/MichaelMure/git-bug/api/graphql/graph" + "github.com/MichaelMure/git-bug/api/graphql/models" "github.com/MichaelMure/git-bug/bug" - "github.com/MichaelMure/git-bug/graphql/connections" - "github.com/MichaelMure/git-bug/graphql/graph" - "github.com/MichaelMure/git-bug/graphql/models" ) var _ graph.BugResolver = &bugResolver{} diff --git a/graphql/resolvers/color.go b/api/graphql/resolvers/color.go index 8dc13095..cfa411f8 100644 --- a/graphql/resolvers/color.go +++ b/api/graphql/resolvers/color.go @@ -4,7 +4,7 @@ import ( "context" "image/color" - "github.com/MichaelMure/git-bug/graphql/graph" + "github.com/MichaelMure/git-bug/api/graphql/graph" ) var _ graph.ColorResolver = &colorResolver{} diff --git a/graphql/resolvers/comment.go b/api/graphql/resolvers/comment.go index b142712a..5206e8a7 100644 --- a/graphql/resolvers/comment.go +++ b/api/graphql/resolvers/comment.go @@ -3,9 +3,9 @@ package resolvers import ( "context" + "github.com/MichaelMure/git-bug/api/graphql/graph" + "github.com/MichaelMure/git-bug/api/graphql/models" "github.com/MichaelMure/git-bug/bug" - "github.com/MichaelMure/git-bug/graphql/graph" - "github.com/MichaelMure/git-bug/graphql/models" ) var _ graph.CommentResolver = &commentResolver{} diff --git a/graphql/resolvers/identity.go b/api/graphql/resolvers/identity.go index b8aa72a7..69a32c98 100644 --- a/graphql/resolvers/identity.go +++ b/api/graphql/resolvers/identity.go @@ -3,8 +3,8 @@ package resolvers import ( "context" - "github.com/MichaelMure/git-bug/graphql/graph" - "github.com/MichaelMure/git-bug/graphql/models" + "github.com/MichaelMure/git-bug/api/graphql/graph" + "github.com/MichaelMure/git-bug/api/graphql/models" ) var _ graph.IdentityResolver = &identityResolver{} diff --git a/graphql/resolvers/label.go b/api/graphql/resolvers/label.go index 0368a1e6..83e95029 100644 --- a/graphql/resolvers/label.go +++ b/api/graphql/resolvers/label.go @@ -5,9 +5,9 @@ import ( "fmt" "image/color" + "github.com/MichaelMure/git-bug/api/graphql/graph" + "github.com/MichaelMure/git-bug/api/graphql/models" "github.com/MichaelMure/git-bug/bug" - "github.com/MichaelMure/git-bug/graphql/graph" - "github.com/MichaelMure/git-bug/graphql/models" ) var _ graph.LabelResolver = &labelResolver{} diff --git a/graphql/resolvers/mutation.go b/api/graphql/resolvers/mutation.go index 31505047..642a4fb9 100644 --- a/graphql/resolvers/mutation.go +++ b/api/graphql/resolvers/mutation.go @@ -4,11 +4,11 @@ import ( "context" "time" + "github.com/MichaelMure/git-bug/api/auth" + "github.com/MichaelMure/git-bug/api/graphql/graph" + "github.com/MichaelMure/git-bug/api/graphql/models" "github.com/MichaelMure/git-bug/bug" "github.com/MichaelMure/git-bug/cache" - "github.com/MichaelMure/git-bug/graphql/graph" - "github.com/MichaelMure/git-bug/graphql/graphqlidentity" - "github.com/MichaelMure/git-bug/graphql/models" ) var _ graph.MutationResolver = &mutationResolver{} @@ -31,11 +31,11 @@ func (r mutationResolver) getBug(repoRef *string, bugPrefix string) (*cache.Repo return nil, nil, err } - bug, err := repo.ResolveBugPrefix(bugPrefix) + b, err := repo.ResolveBugPrefix(bugPrefix) if err != nil { return nil, nil, err } - return repo, bug, nil + return repo, b, nil } func (r mutationResolver) NewBug(ctx context.Context, input models.NewBugInput) (*models.NewBugPayload, error) { @@ -44,12 +44,12 @@ func (r mutationResolver) NewBug(ctx context.Context, input models.NewBugInput) return nil, err } - id, err := graphqlidentity.ForContext(ctx, repo) + author, err := auth.UserFromCtx(ctx, repo) if err != nil { return nil, err } - b, op, err := repo.NewBugRaw(id, time.Now().Unix(), input.Title, input.Message, input.Files, nil) + b, op, err := repo.NewBugRaw(author, time.Now().Unix(), input.Title, input.Message, input.Files, nil) if err != nil { return nil, err } @@ -67,12 +67,12 @@ func (r mutationResolver) AddComment(ctx context.Context, input models.AddCommen return nil, err } - id, err := graphqlidentity.ForContext(ctx, repo) + author, err := auth.UserFromCtx(ctx, repo) if err != nil { return nil, err } - op, err := b.AddCommentRaw(id, time.Now().Unix(), input.Message, input.Files, nil) + op, err := b.AddCommentRaw(author, time.Now().Unix(), input.Message, input.Files, nil) if err != nil { return nil, err } @@ -95,12 +95,12 @@ func (r mutationResolver) ChangeLabels(ctx context.Context, input *models.Change return nil, err } - id, err := graphqlidentity.ForContext(ctx, repo) + author, err := auth.UserFromCtx(ctx, repo) if err != nil { return nil, err } - results, op, err := b.ChangeLabelsRaw(id, time.Now().Unix(), input.Added, input.Removed, nil) + results, op, err := b.ChangeLabelsRaw(author, time.Now().Unix(), input.Added, input.Removed, nil) if err != nil { return nil, err } @@ -129,12 +129,12 @@ func (r mutationResolver) OpenBug(ctx context.Context, input models.OpenBugInput return nil, err } - id, err := graphqlidentity.ForContext(ctx, repo) + author, err := auth.UserFromCtx(ctx, repo) if err != nil { return nil, err } - op, err := b.OpenRaw(id, time.Now().Unix(), nil) + op, err := b.OpenRaw(author, time.Now().Unix(), nil) if err != nil { return nil, err } @@ -157,12 +157,12 @@ func (r mutationResolver) CloseBug(ctx context.Context, input models.CloseBugInp return nil, err } - id, err := graphqlidentity.ForContext(ctx, repo) + author, err := auth.UserFromCtx(ctx, repo) if err != nil { return nil, err } - op, err := b.CloseRaw(id, time.Now().Unix(), nil) + op, err := b.CloseRaw(author, time.Now().Unix(), nil) if err != nil { return nil, err } @@ -185,12 +185,12 @@ func (r mutationResolver) SetTitle(ctx context.Context, input models.SetTitleInp return nil, err } - id, err := graphqlidentity.ForContext(ctx, repo) + author, err := auth.UserFromCtx(ctx, repo) if err != nil { return nil, err } - op, err := b.SetTitleRaw(id, time.Now().Unix(), input.Title, nil) + op, err := b.SetTitleRaw(author, time.Now().Unix(), input.Title, nil) if err != nil { return nil, err } diff --git a/graphql/resolvers/operations.go b/api/graphql/resolvers/operations.go index 29110cf3..8d3e5bba 100644 --- a/graphql/resolvers/operations.go +++ b/api/graphql/resolvers/operations.go @@ -5,9 +5,9 @@ import ( "fmt" "time" + "github.com/MichaelMure/git-bug/api/graphql/graph" + "github.com/MichaelMure/git-bug/api/graphql/models" "github.com/MichaelMure/git-bug/bug" - "github.com/MichaelMure/git-bug/graphql/graph" - "github.com/MichaelMure/git-bug/graphql/models" ) var _ graph.CreateOperationResolver = createOperationResolver{} diff --git a/graphql/resolvers/query.go b/api/graphql/resolvers/query.go index 6fb18638..4ad7ae0c 100644 --- a/graphql/resolvers/query.go +++ b/api/graphql/resolvers/query.go @@ -3,9 +3,9 @@ package resolvers import ( "context" + "github.com/MichaelMure/git-bug/api/graphql/graph" + "github.com/MichaelMure/git-bug/api/graphql/models" "github.com/MichaelMure/git-bug/cache" - "github.com/MichaelMure/git-bug/graphql/graph" - "github.com/MichaelMure/git-bug/graphql/models" ) var _ graph.QueryResolver = &rootQueryResolver{} diff --git a/graphql/resolvers/repo.go b/api/graphql/resolvers/repo.go index 009ccab6..5d96428e 100644 --- a/graphql/resolvers/repo.go +++ b/api/graphql/resolvers/repo.go @@ -3,12 +3,12 @@ package resolvers import ( "context" + "github.com/MichaelMure/git-bug/api/auth" + "github.com/MichaelMure/git-bug/api/graphql/connections" + "github.com/MichaelMure/git-bug/api/graphql/graph" + "github.com/MichaelMure/git-bug/api/graphql/models" "github.com/MichaelMure/git-bug/bug" "github.com/MichaelMure/git-bug/entity" - "github.com/MichaelMure/git-bug/graphql/connections" - "github.com/MichaelMure/git-bug/graphql/graph" - "github.com/MichaelMure/git-bug/graphql/graphqlidentity" - "github.com/MichaelMure/git-bug/graphql/models" "github.com/MichaelMure/git-bug/query" ) @@ -151,8 +151,8 @@ func (repoResolver) Identity(_ context.Context, obj *models.Repository, prefix s } func (repoResolver) UserIdentity(ctx context.Context, obj *models.Repository) (models.IdentityWrapper, error) { - id, err := graphqlidentity.ForContext(ctx, obj.Repo) - if err == graphqlidentity.ErrNotAuthenticated { + id, err := auth.UserFromCtx(ctx, obj.Repo) + if err == auth.ErrNotAuthenticated { return nil, nil } else if err != nil { return nil, err diff --git a/graphql/resolvers/root.go b/api/graphql/resolvers/root.go index 9973ff59..bb3bf5cf 100644 --- a/graphql/resolvers/root.go +++ b/api/graphql/resolvers/root.go @@ -2,31 +2,31 @@ package resolvers import ( + "github.com/MichaelMure/git-bug/api/graphql/graph" "github.com/MichaelMure/git-bug/cache" - "github.com/MichaelMure/git-bug/graphql/graph" ) var _ graph.ResolverRoot = &RootResolver{} type RootResolver struct { - cache.MultiRepoCache + *cache.MultiRepoCache } -func NewRootResolver() *RootResolver { +func NewRootResolver(mrc *cache.MultiRepoCache) *RootResolver { return &RootResolver{ - MultiRepoCache: cache.NewMultiRepoCache(), + MultiRepoCache: mrc, } } func (r RootResolver) Query() graph.QueryResolver { return &rootQueryResolver{ - cache: &r.MultiRepoCache, + cache: r.MultiRepoCache, } } func (r RootResolver) Mutation() graph.MutationResolver { return &mutationResolver{ - cache: &r.MultiRepoCache, + cache: r.MultiRepoCache, } } diff --git a/graphql/resolvers/timeline.go b/api/graphql/resolvers/timeline.go index acf236f8..3223b3a0 100644 --- a/graphql/resolvers/timeline.go +++ b/api/graphql/resolvers/timeline.go @@ -4,9 +4,9 @@ import ( "context" "time" + "github.com/MichaelMure/git-bug/api/graphql/graph" + "github.com/MichaelMure/git-bug/api/graphql/models" "github.com/MichaelMure/git-bug/bug" - "github.com/MichaelMure/git-bug/graphql/graph" - "github.com/MichaelMure/git-bug/graphql/models" ) var _ graph.CommentHistoryStepResolver = commentHistoryStepResolver{} diff --git a/graphql/schema/bug.graphql b/api/graphql/schema/bug.graphql index 03aa95b8..03aa95b8 100644 --- a/graphql/schema/bug.graphql +++ b/api/graphql/schema/bug.graphql diff --git a/graphql/schema/identity.graphql b/api/graphql/schema/identity.graphql index 93154a90..93154a90 100644 --- a/graphql/schema/identity.graphql +++ b/api/graphql/schema/identity.graphql diff --git a/graphql/schema/label.graphql b/api/graphql/schema/label.graphql index 1205915c..1205915c 100644 --- a/graphql/schema/label.graphql +++ b/api/graphql/schema/label.graphql diff --git a/graphql/schema/mutations.graphql b/api/graphql/schema/mutations.graphql index e6b70faf..e6b70faf 100644 --- a/graphql/schema/mutations.graphql +++ b/api/graphql/schema/mutations.graphql diff --git a/graphql/schema/operations.graphql b/api/graphql/schema/operations.graphql index 18e0929c..18e0929c 100644 --- a/graphql/schema/operations.graphql +++ b/api/graphql/schema/operations.graphql diff --git a/graphql/schema/repository.graphql b/api/graphql/schema/repository.graphql index 2b98fe37..2b98fe37 100644 --- a/graphql/schema/repository.graphql +++ b/api/graphql/schema/repository.graphql diff --git a/graphql/schema/root.graphql b/api/graphql/schema/root.graphql index 94a0b530..94a0b530 100644 --- a/graphql/schema/root.graphql +++ b/api/graphql/schema/root.graphql diff --git a/graphql/schema/timeline.graphql b/api/graphql/schema/timeline.graphql index 12462aa3..12462aa3 100644 --- a/graphql/schema/timeline.graphql +++ b/api/graphql/schema/timeline.graphql diff --git a/graphql/schema/types.graphql b/api/graphql/schema/types.graphql index 0182885e..0182885e 100644 --- a/graphql/schema/types.graphql +++ b/api/graphql/schema/types.graphql diff --git a/api/http/git_file_handler.go b/api/http/git_file_handler.go new file mode 100644 index 00000000..6bd6fa85 --- /dev/null +++ b/api/http/git_file_handler.go @@ -0,0 +1,61 @@ +package http + +import ( + "bytes" + "net/http" + "time" + + "github.com/gorilla/mux" + + "github.com/MichaelMure/git-bug/cache" + "github.com/MichaelMure/git-bug/util/git" +) + +// implement a http.Handler that will read and server git blob. +// +// Expected gorilla/mux parameters: +// - "repo" : the ref of the repo or "" for the default one +// - "hash" : the git hash of the file to retrieve +type gitFileHandler struct { + mrc *cache.MultiRepoCache +} + +func NewGitFileHandler(mrc *cache.MultiRepoCache) http.Handler { + return &gitFileHandler{mrc: mrc} +} + +func (gfh *gitFileHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + var repo *cache.RepoCache + var err error + + repoVar := mux.Vars(r)["repo"] + switch repoVar { + case "": + repo, err = gfh.mrc.DefaultRepo() + default: + repo, err = gfh.mrc.ResolveRepo(repoVar) + } + + if err != nil { + http.Error(rw, "invalid repo reference", http.StatusBadRequest) + return + } + + hash := git.Hash(mux.Vars(r)["hash"]) + if !hash.IsValid() { + http.Error(rw, "invalid git hash", http.StatusBadRequest) + return + } + + // TODO: this mean that the whole file will he buffered in memory + // This can be a problem for big files. There might be a way around + // that by implementing a io.ReadSeeker that would read and discard + // data when a seek is called. + data, err := repo.ReadData(hash) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + http.ServeContent(rw, r, "", time.Now(), bytes.NewReader(data)) +} diff --git a/api/http/git_file_handlers_test.go b/api/http/git_file_handlers_test.go new file mode 100644 index 00000000..81d97d61 --- /dev/null +++ b/api/http/git_file_handlers_test.go @@ -0,0 +1,91 @@ +package http + +import ( + "bytes" + "image" + "image/png" + "mime/multipart" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gorilla/mux" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/MichaelMure/git-bug/api/auth" + "github.com/MichaelMure/git-bug/cache" + "github.com/MichaelMure/git-bug/repository" +) + +func TestGitFileHandlers(t *testing.T) { + repo := repository.CreateTestRepo(false) + defer repository.CleanupTestRepos(repo) + + mrc := cache.NewMultiRepoCache() + repoCache, err := mrc.RegisterDefaultRepository(repo) + require.NoError(t, err) + + author, err := repoCache.NewIdentity("test identity", "test@test.org") + require.NoError(t, err) + + err = repoCache.SetUserIdentity(author) + require.NoError(t, err) + + // UPLOAD + + uploadHandler := NewGitUploadFileHandler(mrc) + + img := image.NewNRGBA(image.Rect(0, 0, 50, 50)) + data := &bytes.Buffer{} + err = png.Encode(data, img) + require.NoError(t, err) + + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + part, err := writer.CreateFormFile("uploadfile", "noname") + assert.NoError(t, err) + + _, err = part.Write(data.Bytes()) + assert.NoError(t, err) + + err = writer.Close() + assert.NoError(t, err) + + w := httptest.NewRecorder() + r, _ := http.NewRequest("GET", "/", body) + r.Header.Add("Content-Type", writer.FormDataContentType()) + + // Simulate auth + r = r.WithContext(auth.CtxWithUser(r.Context(), author.Id())) + + // Handler's params + r = mux.SetURLVars(r, map[string]string{ + "repo": "", + }) + + uploadHandler.ServeHTTP(w, r) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, `{"hash":"3426a1488292d8f3f3c59ca679681336542b986f"}`, w.Body.String()) + // DOWNLOAD + + downloadHandler := NewGitFileHandler(mrc) + + w = httptest.NewRecorder() + r, _ = http.NewRequest("GET", "/", nil) + + // Simulate auth + r = r.WithContext(auth.CtxWithUser(r.Context(), author.Id())) + + // Handler's params + r = mux.SetURLVars(r, map[string]string{ + "repo": "", + "hash": "3426a1488292d8f3f3c59ca679681336542b986f", + }) + + downloadHandler.ServeHTTP(w, r) + assert.Equal(t, http.StatusOK, w.Code) + + assert.Equal(t, data.Bytes(), w.Body.Bytes()) +} diff --git a/api/http/git_file_upload_handler.go b/api/http/git_file_upload_handler.go new file mode 100644 index 00000000..1702b8b1 --- /dev/null +++ b/api/http/git_file_upload_handler.go @@ -0,0 +1,104 @@ +package http + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + + "github.com/gorilla/mux" + + "github.com/MichaelMure/git-bug/api/auth" + "github.com/MichaelMure/git-bug/cache" +) + +// implement a http.Handler that will accept and store content into git blob. +// +// Expected gorilla/mux parameters: +// - "repo" : the ref of the repo or "" for the default one +type gitUploadFileHandler struct { + mrc *cache.MultiRepoCache +} + +func NewGitUploadFileHandler(mrc *cache.MultiRepoCache) http.Handler { + return &gitUploadFileHandler{mrc: mrc} +} + +func (gufh *gitUploadFileHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + var repo *cache.RepoCache + var err error + + repoVar := mux.Vars(r)["repo"] + switch repoVar { + case "": + repo, err = gufh.mrc.DefaultRepo() + default: + repo, err = gufh.mrc.ResolveRepo(repoVar) + } + + if err != nil { + http.Error(rw, "invalid repo reference", http.StatusBadRequest) + return + } + + _, err = auth.UserFromCtx(r.Context(), repo) + if err == auth.ErrNotAuthenticated { + http.Error(rw, "read-only mode or not logged in", http.StatusForbidden) + return + } else if err != nil { + http.Error(rw, fmt.Sprintf("loading identity: %v", err), http.StatusInternalServerError) + return + } + + // 100MB (github limit) + var maxUploadSize int64 = 100 * 1000 * 1000 + r.Body = http.MaxBytesReader(rw, r.Body, maxUploadSize) + if err := r.ParseMultipartForm(maxUploadSize); err != nil { + http.Error(rw, "file too big (100MB max)", http.StatusBadRequest) + return + } + + file, _, err := r.FormFile("uploadfile") + if err != nil { + http.Error(rw, "invalid file", http.StatusBadRequest) + return + } + defer file.Close() + fileBytes, err := ioutil.ReadAll(file) + if err != nil { + http.Error(rw, "invalid file", http.StatusBadRequest) + return + } + + filetype := http.DetectContentType(fileBytes) + if filetype != "image/jpeg" && filetype != "image/jpg" && + filetype != "image/gif" && filetype != "image/png" { + http.Error(rw, "invalid file type", http.StatusBadRequest) + return + } + + hash, err := repo.StoreData(fileBytes) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + type response struct { + Hash string `json:"hash"` + } + + resp := response{Hash: string(hash)} + + js, err := json.Marshal(resp) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + rw.Header().Set("Content-Type", "application/json") + _, err = rw.Write(js) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } +} diff --git a/cache/multi_repo_cache.go b/cache/multi_repo_cache.go index a55bbcce..726558d9 100644 --- a/cache/multi_repo_cache.go +++ b/cache/multi_repo_cache.go @@ -13,32 +13,32 @@ type MultiRepoCache struct { repos map[string]*RepoCache } -func NewMultiRepoCache() MultiRepoCache { - return MultiRepoCache{ +func NewMultiRepoCache() *MultiRepoCache { + return &MultiRepoCache{ repos: make(map[string]*RepoCache), } } // RegisterRepository register a named repository. Use this for multi-repo setup -func (c *MultiRepoCache) RegisterRepository(ref string, repo repository.ClockedRepo) error { +func (c *MultiRepoCache) RegisterRepository(ref string, repo repository.ClockedRepo) (*RepoCache, error) { r, err := NewRepoCache(repo) if err != nil { - return err + return nil, err } c.repos[ref] = r - return nil + return r, nil } // RegisterDefaultRepository register a unnamed repository. Use this for mono-repo setup -func (c *MultiRepoCache) RegisterDefaultRepository(repo repository.ClockedRepo) error { +func (c *MultiRepoCache) RegisterDefaultRepository(repo repository.ClockedRepo) (*RepoCache, error) { r, err := NewRepoCache(repo) if err != nil { - return err + return nil, err } c.repos[""] = r - return nil + return r, nil } // DefaultRepo retrieve the default repository diff --git a/cache/repo_cache.go b/cache/repo_cache.go index 4a6b007f..92760bbb 100644 --- a/cache/repo_cache.go +++ b/cache/repo_cache.go @@ -142,6 +142,16 @@ func (c *RepoCache) GetUserEmail() (string, error) { return c.repo.GetUserEmail() } +// ReadData will attempt to read arbitrary data from the given hash +func (c *RepoCache) ReadData(hash git.Hash) ([]byte, error) { + return c.repo.ReadData(hash) +} + +// StoreData will store arbitrary data and return the corresponding hash +func (c *RepoCache) StoreData(data []byte) (git.Hash, error) { + return c.repo.StoreData(data) +} + func (c *RepoCache) lock() error { lockPath := repoLockFilePath(c.repo) diff --git a/commands/webui.go b/commands/webui.go index c07f74fd..83480e08 100644 --- a/commands/webui.go +++ b/commands/webui.go @@ -1,11 +1,8 @@ package commands import ( - "bytes" "context" - "encoding/json" "fmt" - "io/ioutil" "log" "net/http" "os" @@ -18,11 +15,12 @@ import ( "github.com/skratchdot/open-golang/open" "github.com/spf13/cobra" - "github.com/MichaelMure/git-bug/graphql" - "github.com/MichaelMure/git-bug/graphql/graphqlidentity" + "github.com/MichaelMure/git-bug/api/auth" + "github.com/MichaelMure/git-bug/api/graphql" + httpapi "github.com/MichaelMure/git-bug/api/http" + "github.com/MichaelMure/git-bug/cache" "github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/repository" - "github.com/MichaelMure/git-bug/util/git" "github.com/MichaelMure/git-bug/webui" ) @@ -35,15 +33,6 @@ var ( const webUIOpenConfigKey = "git-bug.webui.open" -func authMiddleware(id *identity.Identity) func(http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ctx := graphqlidentity.AttachToContext(r.Context(), id) - next.ServeHTTP(w, r.WithContext(ctx)) - }) - } -} - func runWebUI(cmd *cobra.Command, args []string) error { if webUIPort == 0 { var err error @@ -53,38 +42,36 @@ func runWebUI(cmd *cobra.Command, args []string) error { } } - var id *identity.Identity + addr := fmt.Sprintf("127.0.0.1:%d", webUIPort) + webUiAddr := fmt.Sprintf("http://%s", addr) + + router := mux.NewRouter() + + // If the webUI is not read-only, use an authentication middleware with a + // fixed identity: the default user of the repo + // TODO: support dynamic authentication with OAuth if !webUIReadOnly { - // Verify that we have an identity. - var err error - id, err = identity.GetUserIdentity(repo) + author, err := identity.GetUserIdentity(repo) if err != nil { return err } + router.Use(auth.Middleware(author.Id())) } - addr := fmt.Sprintf("127.0.0.1:%d", webUIPort) - webUiAddr := fmt.Sprintf("http://%s", addr) - - router := mux.NewRouter() - - graphqlHandler, err := graphql.NewHandler(repo) + mrc := cache.NewMultiRepoCache() + _, err := mrc.RegisterDefaultRepository(repo) if err != nil { return err } - assetsHandler := &fileSystemWithDefault{ - FileSystem: webui.WebUIAssets, - defaultFile: "index.html", - } + graphqlHandler := graphql.NewHandler(mrc) // Routes router.Path("/playground").Handler(playground.Handler("git-bug", "/graphql")) router.Path("/graphql").Handler(graphqlHandler) - router.Path("/gitfile/{hash}").Handler(newGitFileHandler(repo)) - router.Path("/upload").Methods("POST").Handler(newGitUploadFileHandler(repo)) - router.PathPrefix("/").Handler(http.FileServer(assetsHandler)) - router.Use(authMiddleware(id)) + router.Path("/gitfile/{repo}/{hash}").Handler(httpapi.NewGitFileHandler(mrc)) + router.Path("/upload/{repo}").Methods("POST").Handler(httpapi.NewGitUploadFileHandler(mrc)) + router.PathPrefix("/").Handler(webui.NewHandler()) srv := &http.Server{ Addr: addr, @@ -151,128 +138,6 @@ func runWebUI(cmd *cobra.Command, args []string) error { return nil } -// implement a http.FileSystem that will serve a default file when the looked up -// file doesn't exist. Useful for Single-Page App that implement routing client -// side, where the server has to return the root index.html file for every route. -type fileSystemWithDefault struct { - http.FileSystem - defaultFile string -} - -func (fswd *fileSystemWithDefault) Open(name string) (http.File, error) { - f, err := fswd.FileSystem.Open(name) - if os.IsNotExist(err) { - return fswd.FileSystem.Open(fswd.defaultFile) - } - return f, err -} - -// implement a http.Handler that will read and server git blob. -type gitFileHandler struct { - repo repository.Repo -} - -func newGitFileHandler(repo repository.Repo) http.Handler { - return &gitFileHandler{ - repo: repo, - } -} - -func (gfh *gitFileHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { - hash := git.Hash(mux.Vars(r)["hash"]) - - if !hash.IsValid() { - http.Error(rw, "invalid git hash", http.StatusBadRequest) - return - } - - // TODO: this mean that the whole file will he buffered in memory - // This can be a problem for big files. There might be a way around - // that by implementing a io.ReadSeeker that would read and discard - // data when a seek is called. - data, err := gfh.repo.ReadData(git.Hash(hash)) - if err != nil { - http.Error(rw, err.Error(), http.StatusInternalServerError) - return - } - - http.ServeContent(rw, r, "", time.Now(), bytes.NewReader(data)) -} - -// implement a http.Handler that will accept and store content into git blob. -type gitUploadFileHandler struct { - repo repository.Repo -} - -func newGitUploadFileHandler(repo repository.Repo) http.Handler { - return &gitUploadFileHandler{ - repo: repo, - } -} - -func (gufh *gitUploadFileHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { - _, err := graphqlidentity.ForContextUncached(r.Context(), gufh.repo) - if err == graphqlidentity.ErrNotAuthenticated { - http.Error(rw, fmt.Sprintf("read-only mode or not logged in"), http.StatusForbidden) - return - } else if err != nil { - http.Error(rw, fmt.Sprintf("loading identity: %v", err), http.StatusInternalServerError) - return - } - - // 100MB (github limit) - var maxUploadSize int64 = 100 * 1000 * 1000 - r.Body = http.MaxBytesReader(rw, r.Body, maxUploadSize) - if err := r.ParseMultipartForm(maxUploadSize); err != nil { - http.Error(rw, "file too big (100MB max)", http.StatusBadRequest) - return - } - - file, _, err := r.FormFile("uploadfile") - if err != nil { - http.Error(rw, "invalid file", http.StatusBadRequest) - return - } - defer file.Close() - fileBytes, err := ioutil.ReadAll(file) - if err != nil { - http.Error(rw, "invalid file", http.StatusBadRequest) - return - } - - filetype := http.DetectContentType(fileBytes) - if filetype != "image/jpeg" && filetype != "image/jpg" && - filetype != "image/gif" && filetype != "image/png" { - http.Error(rw, "invalid file type", http.StatusBadRequest) - return - } - - hash, err := gufh.repo.StoreData(fileBytes) - if err != nil { - http.Error(rw, err.Error(), http.StatusInternalServerError) - return - } - - type response struct { - Hash string `json:"hash"` - } - - resp := response{Hash: string(hash)} - - js, err := json.Marshal(resp) - if err != nil { - http.Error(rw, err.Error(), http.StatusInternalServerError) - return - } - - rw.Header().Set("Content-Type", "application/json") - _, err = rw.Write(js) - if err != nil { - http.Error(rw, err.Error(), http.StatusInternalServerError) - return - } -} - var webUICmd = &cobra.Command{ Use: "webui", Short: "Launch the web UI.", @@ -294,5 +159,4 @@ func init() { webUICmd.Flags().BoolVar(&webUINoOpen, "no-open", false, "Prevent the automatic opening of the web UI in the default browser") webUICmd.Flags().IntVarP(&webUIPort, "port", "p", 0, "Port to listen to (default is random)") webUICmd.Flags().BoolVar(&webUIReadOnly, "read-only", false, "Whether to run the web UI in read-only mode") - } diff --git a/graphql/graphqlidentity/graphqlidentity.go b/graphql/graphqlidentity/graphqlidentity.go deleted file mode 100644 index 36b496f3..00000000 --- a/graphql/graphqlidentity/graphqlidentity.go +++ /dev/null @@ -1,41 +0,0 @@ -// Package graphqlidentity contains helpers for managing identities within the GraphQL API. -package graphqlidentity - -import ( - "context" - - "github.com/MichaelMure/git-bug/cache" - "github.com/MichaelMure/git-bug/entity" - "github.com/MichaelMure/git-bug/identity" - "github.com/MichaelMure/git-bug/repository" -) - -// identityCtxKey is a unique context key, accessible only in this package. -var identityCtxKey = &struct{}{} - -// AttachToContext attaches an Identity to a context. -func AttachToContext(ctx context.Context, u *identity.Identity) context.Context { - return context.WithValue(ctx, identityCtxKey, u.Id()) -} - -// ForContext retrieves an IdentityCache from the context. -// If there is no identity in the context, ErrNotAuthenticated is returned. -// If an error occurs while resolving the identity (e.g. I/O error), then it will be returned. -func ForContext(ctx context.Context, r *cache.RepoCache) (*cache.IdentityCache, error) { - id, ok := ctx.Value(identityCtxKey).(entity.Id) - if !ok { - return nil, ErrNotAuthenticated - } - return r.ResolveIdentity(id) -} - -// ForContextUncached retrieves an Identity from the context. -// If there is no identity in the context, ErrNotAuthenticated is returned. -// If an error occurs while resolving the identity (e.g. I/O error), then it will be returned. -func ForContextUncached(ctx context.Context, repo repository.Repo) (*identity.Identity, error) { - id, ok := ctx.Value(identityCtxKey).(entity.Id) - if !ok { - return nil, ErrNotAuthenticated - } - return identity.ReadLocal(repo, id) -} diff --git a/graphql/handler.go b/graphql/handler.go deleted file mode 100644 index 55ef6fc4..00000000 --- a/graphql/handler.go +++ /dev/null @@ -1,39 +0,0 @@ -//go:generate go run gen_graphql.go - -// Package graphql contains the root GraphQL http handler -package graphql - -import ( - "net/http" - - "github.com/99designs/gqlgen/graphql/handler" - - "github.com/MichaelMure/git-bug/graphql/graph" - "github.com/MichaelMure/git-bug/graphql/resolvers" - "github.com/MichaelMure/git-bug/repository" -) - -// Handler is the root GraphQL http handler -type Handler struct { - http.Handler - *resolvers.RootResolver -} - -func NewHandler(repo repository.ClockedRepo) (Handler, error) { - h := Handler{ - RootResolver: resolvers.NewRootResolver(), - } - - err := h.RootResolver.RegisterDefaultRepository(repo) - if err != nil { - return Handler{}, err - } - - config := graph.Config{ - Resolvers: h.RootResolver, - } - - h.Handler = handler.NewDefaultServer(graph.NewExecutableSchema(config)) - - return h, nil -} diff --git a/webui/codegen.yaml b/webui/codegen.yaml index 3cdb0517..1c2a91a8 100644 --- a/webui/codegen.yaml +++ b/webui/codegen.yaml @@ -1,4 +1,4 @@ -schema: '../graphql/schema/*.graphql' +schema: '../api/graphql/schema/*.graphql' overwrite: true documents: src/**/*.graphql generates: diff --git a/webui/handler.go b/webui/handler.go new file mode 100644 index 00000000..476a46cf --- /dev/null +++ b/webui/handler.go @@ -0,0 +1,31 @@ +package webui + +import ( + "net/http" + "os" +) + +// implement a http.FileSystem that will serve a default file when the looked up +// file doesn't exist. Useful for Single-Page App that implement routing client +// side, where the server has to return the root index.html file for every route. +type fileSystemWithDefault struct { + http.FileSystem + defaultFile string +} + +func (fswd *fileSystemWithDefault) Open(name string) (http.File, error) { + f, err := fswd.FileSystem.Open(name) + if os.IsNotExist(err) { + return fswd.FileSystem.Open(fswd.defaultFile) + } + return f, err +} + +func NewHandler() http.Handler { + assetsHandler := &fileSystemWithDefault{ + FileSystem: WebUIAssets, + defaultFile: "index.html", + } + + return http.FileServer(assetsHandler) +} |