From 2ab6381a94d55fa22b80acdbb18849d6b24951f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Sun, 21 Jun 2020 22:12:04 +0200 Subject: [PATCH] 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 --- README.md | 2 +- api/auth/context.go | 28 +++ .../graphqlidentity => api/auth}/errors.go | 2 +- api/auth/middleware.go | 16 ++ .../connections/connection_template.go | 2 +- .../graphql}/connections/connections.go | 0 {graphql => api/graphql}/connections/edges.go | 0 .../graphql}/connections/gen_comment.go | 2 +- .../graphql}/connections/gen_identity.go | 2 +- .../graphql}/connections/gen_label.go | 2 +- .../graphql}/connections/gen_lazy_bug.go | 2 +- .../graphql}/connections/gen_lazy_identity.go | 2 +- .../graphql}/connections/gen_operation.go | 2 +- .../graphql}/connections/gen_timeline.go | 2 +- {graphql => api/graphql}/gen_graphql.go | 0 {graphql => api/graphql}/gqlgen.yml | 0 {graphql => api/graphql}/graph/gen_graph.go | 7 +- {graphql => api/graphql}/graphql_test.go | 17 +- api/graphql/handler.go | 32 ++++ {graphql => api/graphql}/models/edges.go | 0 {graphql => api/graphql}/models/gen_models.go | 0 {graphql => api/graphql}/models/lazy_bug.go | 0 .../graphql}/models/lazy_identity.go | 0 {graphql => api/graphql}/models/models.go | 0 {graphql => api/graphql}/resolvers/bug.go | 6 +- {graphql => api/graphql}/resolvers/color.go | 2 +- {graphql => api/graphql}/resolvers/comment.go | 4 +- .../graphql}/resolvers/identity.go | 4 +- {graphql => api/graphql}/resolvers/label.go | 4 +- .../graphql}/resolvers/mutation.go | 34 ++-- .../graphql}/resolvers/operations.go | 4 +- {graphql => api/graphql}/resolvers/query.go | 4 +- {graphql => api/graphql}/resolvers/repo.go | 12 +- {graphql => api/graphql}/resolvers/root.go | 12 +- .../graphql}/resolvers/timeline.go | 4 +- {graphql => api/graphql}/schema/bug.graphql | 0 .../graphql}/schema/identity.graphql | 0 {graphql => api/graphql}/schema/label.graphql | 0 .../graphql}/schema/mutations.graphql | 0 .../graphql}/schema/operations.graphql | 0 .../graphql}/schema/repository.graphql | 0 {graphql => api/graphql}/schema/root.graphql | 0 .../graphql}/schema/timeline.graphql | 0 {graphql => api/graphql}/schema/types.graphql | 0 api/http/git_file_handler.go | 61 ++++++ api/http/git_file_handlers_test.go | 91 +++++++++ api/http/git_file_upload_handler.go | 104 +++++++++++ cache/multi_repo_cache.go | 16 +- cache/repo_cache.go | 10 + commands/webui.go | 176 ++---------------- graphql/graphqlidentity/graphqlidentity.go | 41 ---- graphql/handler.go | 39 ---- webui/codegen.yaml | 2 +- webui/handler.go | 31 +++ 54 files changed, 472 insertions(+), 309 deletions(-) create mode 100644 api/auth/context.go rename {graphql/graphqlidentity => api/auth}/errors.go (90%) create mode 100644 api/auth/middleware.go rename {graphql => api/graphql}/connections/connection_template.go (98%) rename {graphql => api/graphql}/connections/connections.go (100%) rename {graphql => api/graphql}/connections/edges.go (100%) rename {graphql => api/graphql}/connections/gen_comment.go (98%) rename {graphql => api/graphql}/connections/gen_identity.go (98%) rename {graphql => api/graphql}/connections/gen_label.go (98%) rename {graphql => api/graphql}/connections/gen_lazy_bug.go (98%) rename {graphql => api/graphql}/connections/gen_lazy_identity.go (98%) rename {graphql => api/graphql}/connections/gen_operation.go (98%) rename {graphql => api/graphql}/connections/gen_timeline.go (98%) rename {graphql => api/graphql}/gen_graphql.go (100%) rename {graphql => api/graphql}/gqlgen.yml (100%) rename {graphql => api/graphql}/graph/gen_graph.go (99%) rename {graphql => api/graphql}/graphql_test.go (92%) create mode 100644 api/graphql/handler.go rename {graphql => api/graphql}/models/edges.go (100%) rename {graphql => api/graphql}/models/gen_models.go (100%) rename {graphql => api/graphql}/models/lazy_bug.go (100%) rename {graphql => api/graphql}/models/lazy_identity.go (100%) rename {graphql => api/graphql}/models/models.go (100%) rename {graphql => api/graphql}/resolvers/bug.go (96%) rename {graphql => api/graphql}/resolvers/color.go (89%) rename {graphql => api/graphql}/resolvers/comment.go (75%) rename {graphql => api/graphql}/resolvers/identity.go (78%) rename {graphql => api/graphql}/resolvers/label.go (91%) rename {graphql => api/graphql}/resolvers/mutation.go (79%) rename {graphql => api/graphql}/resolvers/operations.go (97%) rename {graphql => api/graphql}/resolvers/query.go (88%) rename {graphql => api/graphql}/resolvers/repo.go (94%) rename {graphql => api/graphql}/resolvers/root.go (91%) rename {graphql => api/graphql}/resolvers/timeline.go (97%) rename {graphql => api/graphql}/schema/bug.graphql (100%) rename {graphql => api/graphql}/schema/identity.graphql (100%) rename {graphql => api/graphql}/schema/label.graphql (100%) rename {graphql => api/graphql}/schema/mutations.graphql (100%) rename {graphql => api/graphql}/schema/operations.graphql (100%) rename {graphql => api/graphql}/schema/repository.graphql (100%) rename {graphql => api/graphql}/schema/root.graphql (100%) rename {graphql => api/graphql}/schema/timeline.graphql (100%) rename {graphql => api/graphql}/schema/types.graphql (100%) create mode 100644 api/http/git_file_handler.go create mode 100644 api/http/git_file_handlers_test.go create mode 100644 api/http/git_file_upload_handler.go delete mode 100644 graphql/graphqlidentity/graphqlidentity.go delete mode 100644 graphql/handler.go create mode 100644 webui/handler.go diff --git a/README.md b/README.md index bb2750a2451a187fbb47857fff3d101acbb943b4..52a734bfaa69f5a09a0315e01181cc2e2f086175 100644 --- a/README.md +++ b/README.md @@ -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 0000000000000000000000000000000000000000..1717126128622dc9cc587c4742786c4bd15ea363 --- /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 similarity index 90% rename from graphql/graphqlidentity/errors.go rename to api/auth/errors.go index 5ec58b32e8b289eba82d00ec9fa12ef42f8a5171..9675afbf4d62c5e8d5badab729917009e86b737f 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 0000000000000000000000000000000000000000..d1d654cea60d51056c63285b29cb652f95d86f8f --- /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 similarity index 98% rename from graphql/connections/connection_template.go rename to api/graphql/connections/connection_template.go index f276b2d0cfa5607c1dd787c79cbc410940590def..935a0a77ffdbc757bd26608ee2e1a655c4c1283f 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 similarity index 100% rename from graphql/connections/connections.go rename to api/graphql/connections/connections.go diff --git a/graphql/connections/edges.go b/api/graphql/connections/edges.go similarity index 100% rename from graphql/connections/edges.go rename to api/graphql/connections/edges.go diff --git a/graphql/connections/gen_comment.go b/api/graphql/connections/gen_comment.go similarity index 98% rename from graphql/connections/gen_comment.go rename to api/graphql/connections/gen_comment.go index 9f96f2bb8b6d6a53e2e5a90aa555ecd5ded53c52..bae740304a7e13f43f1b5c1e3be48ea7121e3fc3 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 similarity index 98% rename from graphql/connections/gen_identity.go rename to api/graphql/connections/gen_identity.go index 061e8936f028ce49e504f3f7064cb8824576e967..2138b17ec02794b2d7f8314315c225c119b7d06a 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 similarity index 98% rename from graphql/connections/gen_label.go rename to api/graphql/connections/gen_label.go index 7f1b2fc957eb3379d6c431ac8b4688fb9056726e..39b1c536bd4b39f25ba1a892db23116dc5be2175 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 similarity index 98% rename from graphql/connections/gen_lazy_bug.go rename to api/graphql/connections/gen_lazy_bug.go index 9638e86b82b94613ed100426db4a790b927c1a0d..1dc4692ebd53fcf32d7823069ec23bf23d9e86d3 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 similarity index 98% rename from graphql/connections/gen_lazy_identity.go rename to api/graphql/connections/gen_lazy_identity.go index 932d802c81f8bc5c4402c02acbaff29e7765ae68..4996e2199754881e11e90b1e088b988ef48d0981 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 similarity index 98% rename from graphql/connections/gen_operation.go rename to api/graphql/connections/gen_operation.go index 0f40e2c454f7b3126572aad1a7cf820c1253dd53..4bd84895950eb063e8e29d575b778713f2405a59 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 similarity index 98% rename from graphql/connections/gen_timeline.go rename to api/graphql/connections/gen_timeline.go index 01dac96b3fed81716e089631abf3f8f650d840a0..952d095ce01102c73f53c83d732d7b4a8c5d60d9 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 similarity index 100% rename from graphql/gen_graphql.go rename to api/graphql/gen_graphql.go diff --git a/graphql/gqlgen.yml b/api/graphql/gqlgen.yml similarity index 100% rename from graphql/gqlgen.yml rename to api/graphql/gqlgen.yml diff --git a/graphql/graph/gen_graph.go b/api/graphql/graph/gen_graph.go similarity index 99% rename from graphql/graph/gen_graph.go rename to api/graphql/graph/gen_graph.go index 277cb7cb7e6e8536e6dff37847ca7f9a8d768ecd..be0e92ee9f89204ede5c5cc5e4d2b8ab4273ab8a 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 similarity index 92% rename from graphql/graphql_test.go rename to api/graphql/graphql_test.go index 0ff2c3fbc247e9c1c6e5d6f5632a953a8ef787f7..45e88e9af8f45666c27f6d3bb0a41c5f49ccbb3e 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 0000000000000000000000000000000000000000..03dc32e9177072738b4885987d52155998026e73 --- /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 similarity index 100% rename from graphql/models/edges.go rename to api/graphql/models/edges.go diff --git a/graphql/models/gen_models.go b/api/graphql/models/gen_models.go similarity index 100% rename from graphql/models/gen_models.go rename to api/graphql/models/gen_models.go diff --git a/graphql/models/lazy_bug.go b/api/graphql/models/lazy_bug.go similarity index 100% rename from graphql/models/lazy_bug.go rename to api/graphql/models/lazy_bug.go diff --git a/graphql/models/lazy_identity.go b/api/graphql/models/lazy_identity.go similarity index 100% rename from graphql/models/lazy_identity.go rename to api/graphql/models/lazy_identity.go diff --git a/graphql/models/models.go b/api/graphql/models/models.go similarity index 100% rename from graphql/models/models.go rename to api/graphql/models/models.go diff --git a/graphql/resolvers/bug.go b/api/graphql/resolvers/bug.go similarity index 96% rename from graphql/resolvers/bug.go rename to api/graphql/resolvers/bug.go index fd8f4b6ea61bd6ab033070f4dc7d4a9fce88e4cb..815cba8d443962899d663c49b7aa9a3d24ca0c90 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 similarity index 89% rename from graphql/resolvers/color.go rename to api/graphql/resolvers/color.go index 8dc1309536def0b29a0658eec6512be985a1a461..cfa411f8f1a7068a08cc4106493009919fdc262c 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 similarity index 75% rename from graphql/resolvers/comment.go rename to api/graphql/resolvers/comment.go index b142712ad82ab104f9575ed9c598b3f26ac6f2f7..5206e8a793007ed842f782b58b59e5732f9eecf1 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 similarity index 78% rename from graphql/resolvers/identity.go rename to api/graphql/resolvers/identity.go index b8aa72a7983a1041b3108c774102bea57e5124b6..69a32c98c97f335c6d114c327c4c0c40e5b34e6f 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 similarity index 91% rename from graphql/resolvers/label.go rename to api/graphql/resolvers/label.go index 0368a1e603e67c89a3178ebfbcffe3248ca6d395..83e95029f2c6965b688b15e98e90285a34a2ba9a 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 similarity index 79% rename from graphql/resolvers/mutation.go rename to api/graphql/resolvers/mutation.go index 315050475b6999286283ff7804b1beac526adde8..642a4fb981f604aeedb7ed0e6858b56d985c0ddd 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 similarity index 97% rename from graphql/resolvers/operations.go rename to api/graphql/resolvers/operations.go index 29110cf39fa80f2d07593d8d746404e7ff699301..8d3e5bba2783b41a3fe7c73959266a6e39c64beb 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 similarity index 88% rename from graphql/resolvers/query.go rename to api/graphql/resolvers/query.go index 6fb1863806855226cda8cb4f63522261ca2c1693..4ad7ae0c9104cb47401b8ef42fe37f3c54fcf966 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 similarity index 94% rename from graphql/resolvers/repo.go rename to api/graphql/resolvers/repo.go index 009ccab60f411c2bc3e5835ff578c92047e91906..5d96428e6b86ef56fad6c0943e2d822ce4184da8 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 similarity index 91% rename from graphql/resolvers/root.go rename to api/graphql/resolvers/root.go index 9973ff59afe83cb4f7d0a1b9387a66afa16d127c..bb3bf5cfbb8e4037f37ec1ac4e83573bc43526ca 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 similarity index 97% rename from graphql/resolvers/timeline.go rename to api/graphql/resolvers/timeline.go index acf236f8ada7df9f50efb425eea7ad1922db1ea8..3223b3a0a0ba8728446f47578b7a226a53a17021 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 similarity index 100% rename from graphql/schema/bug.graphql rename to api/graphql/schema/bug.graphql diff --git a/graphql/schema/identity.graphql b/api/graphql/schema/identity.graphql similarity index 100% rename from graphql/schema/identity.graphql rename to api/graphql/schema/identity.graphql diff --git a/graphql/schema/label.graphql b/api/graphql/schema/label.graphql similarity index 100% rename from graphql/schema/label.graphql rename to api/graphql/schema/label.graphql diff --git a/graphql/schema/mutations.graphql b/api/graphql/schema/mutations.graphql similarity index 100% rename from graphql/schema/mutations.graphql rename to api/graphql/schema/mutations.graphql diff --git a/graphql/schema/operations.graphql b/api/graphql/schema/operations.graphql similarity index 100% rename from graphql/schema/operations.graphql rename to api/graphql/schema/operations.graphql diff --git a/graphql/schema/repository.graphql b/api/graphql/schema/repository.graphql similarity index 100% rename from graphql/schema/repository.graphql rename to api/graphql/schema/repository.graphql diff --git a/graphql/schema/root.graphql b/api/graphql/schema/root.graphql similarity index 100% rename from graphql/schema/root.graphql rename to api/graphql/schema/root.graphql diff --git a/graphql/schema/timeline.graphql b/api/graphql/schema/timeline.graphql similarity index 100% rename from graphql/schema/timeline.graphql rename to api/graphql/schema/timeline.graphql diff --git a/graphql/schema/types.graphql b/api/graphql/schema/types.graphql similarity index 100% rename from graphql/schema/types.graphql rename to 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 0000000000000000000000000000000000000000..6bd6fa85ee6e7abaa1d007f4611c51d8b520d559 --- /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 0000000000000000000000000000000000000000..81d97d61c52636c75a47c830bea7fc7dcb4d7960 --- /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 0000000000000000000000000000000000000000..1702b8b1d851f4aa5651d35c603722622da9c657 --- /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 a55bbcce162c946c2a8ead40b118752d05bca704..726558d9b763a795f3b68f852ecfad7e2c58fb5f 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 4a6b007f43851b579f6fe034bff640ce528e9e50..92760bbb9a8c64c182580630a0f088252deaf641 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 c07f74fd6a3d9b7080f80183f3d6679269fd5f91..83480e08e20b0f245dc56ccdb75743286e77cd0e 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 36b496f38abd33338b6246cf01a4f24193ace2ae..0000000000000000000000000000000000000000 --- 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 55ef6fc49896f029bf35175550a6c3245c2c08bd..0000000000000000000000000000000000000000 --- 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 3cdb05176f0cf3ec74cde96a41e42a8b90d703f3..1c2a91a8566ddbc5aff4908a80aa5dda47578785 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 0000000000000000000000000000000000000000..476a46cf0f9aa8a64fd64c2a87827c3142ba8aaa --- /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) +}