From daa6f9b108f4e6ecb55378c000da4f696d35b68d Mon Sep 17 00:00:00 2001 From: Drew DeVault Date: Tue, 19 May 2020 12:00:27 -0400 Subject: [PATCH] API: refactor logic out into gql.sr.ht module --- api/auth/auth.go | 260 ---------------------------------- api/crypto/crypto.go | 60 -------- api/database/filter.go | 51 ------- api/database/ql.go | 97 ------------- api/database/sq.go | 30 ---- api/go.mod | 12 +- api/go.sum | 24 ++++ api/graph/model/acl.go | 2 +- api/graph/model/cursor.go | 2 +- api/graph/model/repository.go | 2 +- api/graph/model/user.go | 2 +- api/graph/recover.go | 127 ----------------- api/graph/resolver.go | 8 +- api/graph/schema.resolvers.go | 14 +- api/loaders/middleware.go | 69 +++++---- api/server.go | 132 +---------------- 16 files changed, 84 insertions(+), 808 deletions(-) delete mode 100644 api/auth/auth.go delete mode 100644 api/crypto/crypto.go delete mode 100644 api/database/filter.go delete mode 100644 api/database/ql.go delete mode 100644 api/database/sq.go delete mode 100644 api/graph/recover.go diff --git a/api/auth/auth.go b/api/auth/auth.go deleted file mode 100644 index 48cdde3..0000000 --- a/api/auth/auth.go +++ /dev/null @@ -1,260 +0,0 @@ -package auth - -import ( - "context" - "crypto/sha512" - "database/sql" - "encoding/hex" - "encoding/json" - "errors" - "fmt" - "net/http" - "regexp" - "strings" - "time" - - "github.com/vektah/gqlparser/gqlerror" - - "git.sr.ht/~sircmpwn/git.sr.ht/api/crypto" - "git.sr.ht/~sircmpwn/git.sr.ht/api/database" -) - -var userCtxKey = &contextKey{"user"} - -type contextKey struct { - name string -} - -var bearerRegex = regexp.MustCompile(`^[0-9a-f]{32}$`) - -const ( - USER_UNCONFIRMED = "unconfirmed" - USER_ACTIVE_NON_PAYING = "active_non_paying" - USER_ACTIVE_FREE = "active_free" - USER_ACTIVE_PAYING = "active_paying" - USER_ACTIVE_DELINQUENT = "active_delinquent" - USER_ADMIN = "admin" - USER_UNKNOWN = "unknown" - USER_SUSPENDED = "suspended" -) - -type User struct { - ID int - Created time.Time - Updated time.Time - Username string - Email string - UserType string - URL *string - Location *string - Bio *string - SuspensionNotice *string -} - -func authError(w http.ResponseWriter, reason string, code int) { - gqlerr := gqlerror.Errorf("Authentication error: %s", reason) - b, err := json.Marshal(gqlerr) - if err != nil { - panic(err) - } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(code) - w.Write(b) -} - -type AuthCookie struct { - Name string `json:"name"` -} - -func cookieAuth(db *sql.DB, cookie *http.Cookie, - w http.ResponseWriter, r *http.Request, next http.Handler) { - - payload := crypto.Decrypt([]byte(cookie.Value)) - if payload == nil { - authError(w, "Invalid authentication cookie", http.StatusForbidden) - return - } - - var ( - auth AuthCookie - err error - rows *sql.Rows - user User - ) - if err := json.Unmarshal(payload, &auth); err != nil { - authError(w, "Invalid authentication cookie", http.StatusForbidden) - return - } - - query := database. - Select(context.TODO(), []string{ - `u.id`, `u.username`, - `u.created`, `u.updated`, - `u.email`, - `u.user_type`, - `u.url`, `u.location`, `u.bio`, - `u.suspension_notice`, - }). - From(`"user" u`). - Where(`u.username = ?`, auth.Name) - if rows, err = query.RunWith(db).Query(); err != nil { - panic(err) - } - defer rows.Close() - - if !rows.Next() { - if err := rows.Err(); err != nil { - panic(err) - } - authError(w, "Invalid or expired OAuth token", http.StatusForbidden) - return - } - if err := rows.Scan(&user.ID, &user.Username, &user.Created, &user.Updated, - &user.Email, &user.UserType, &user.URL, &user.Location, &user.Bio, - &user.SuspensionNotice); err != nil { - panic(err) - } - if rows.Next() { - if err := rows.Err(); err != nil { - panic(err) - } - panic(errors.New("Multiple matching user accounts; invariant broken")) - } - - if user.UserType == USER_SUSPENDED { - authError(w, fmt.Sprintf("Account suspended with the following notice: %s\nContact support", - user.SuspensionNotice), http.StatusForbidden) - return - } - - ctx := context.WithValue(r.Context(), userCtxKey, &user) - - r = r.WithContext(ctx) - next.ServeHTTP(w, r) -} - -func Middleware(db *sql.DB) func(http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/query" { - next.ServeHTTP(w, r) - return - } - - cookie, err := r.Cookie("sr.ht.unified-login.v1") - if err == nil { - cookieAuth(db, cookie, w, r, next) - return - } - - auth := r.Header.Get("Authorization") - if auth == "" { - authError(w, `Authorization header is required. -Expected 'Authorization: Bearer '`, http.StatusForbidden) - return - } - - z := strings.SplitN(auth, " ", 2) - if len(z) != 2 { - authError(w, "Invalid Authorization header", http.StatusBadRequest) - return - } - - var bearer string - switch z[0] { - case "Bearer": - token := []byte(z[1]) - if !bearerRegex.Match(token) { - authError(w, "Invalid bearer token, expected 32-character haxadecimal string", http.StatusBadRequest) - return - } - hash := sha512.Sum512(token) - bearer = hex.EncodeToString(hash[:]) - case "Internal": - panic(errors.New("TODO")) - default: - authError(w, "Invalid Authorization header", http.StatusBadRequest) - return - } - - var ( - expires time.Time - rows *sql.Rows - scopes string - user User - ) - query := database. - Select(context.TODO(), []string{ - `ot.expires`, - `ot.scopes`, - `u.id`, `u.username`, - `u.created`, `u.updated`, - `u.email`, - `u.user_type`, - `u.url`, `u.location`, `u.bio`, - `u.suspension_notice`, - }). - From(`oauthtoken ot`). - Join(`"user" u ON u.id = ot.user_id`). - Where(`ot.token_hash = ?`, bearer) - if rows, err = query.RunWith(db).Query(); err != nil { - panic(err) - } - defer rows.Close() - - if !rows.Next() { - if err := rows.Err(); err != nil { - panic(err) - } - authError(w, "Invalid or expired OAuth token", http.StatusForbidden) - return - } - if err := rows.Scan(&expires, &scopes, - &user.ID, &user.Username, - &user.Created, &user.Updated, - &user.Email, - &user.UserType, - &user.URL, - &user.Location, - &user.Bio, - &user.SuspensionNotice); err != nil { - panic(err) - } - if rows.Next() { - if err := rows.Err(); err != nil { - panic(err) - } - panic(errors.New("Multiple matching OAuth tokens; invariant broken")) - } - - if time.Now().UTC().After(expires) { - authError(w, "Invalid or expired OAuth token", http.StatusForbidden) - return - } - - if user.UserType == USER_SUSPENDED { - authError(w, fmt.Sprintf("Account suspended with the following notice: %s\nContact support", - user.SuspensionNotice), http.StatusForbidden) - return - } - - if scopes != "*" && scopes != "*:read" && scopes != "*:write" { - authError(w, "Presently, OAuth authentication to the GraphQL API is only supported for OAuth tokens with all permissions, namely '*'.", http.StatusForbidden) - return - } - - ctx := context.WithValue(r.Context(), userCtxKey, &user) - - r = r.WithContext(ctx) - next.ServeHTTP(w, r) - }) - } -} - -func ForContext(ctx context.Context) *User { - raw, ok := ctx.Value(userCtxKey).(*User) - if !ok { - panic(errors.New("Invalid authentication context")) - } - return raw -} diff --git a/api/crypto/crypto.go b/api/crypto/crypto.go deleted file mode 100644 index 553fb75..0000000 --- a/api/crypto/crypto.go +++ /dev/null @@ -1,60 +0,0 @@ -package crypto - -import ( - "crypto/ed25519" - "encoding/base64" - "log" - "time" - - "github.com/fernet/fernet-go" - "github.com/vaughan0/go-ini" -) - -var ( - privateKey ed25519.PrivateKey - publicKey ed25519.PublicKey - fernetKey *fernet.Key -) - -func InitCrypto(config ini.File) { - b64key, ok := config.Get("webhooks", "private-key") - if !ok { - log.Fatalf("No webhook key configured") - } - seed, err := base64.StdEncoding.DecodeString(b64key) - if err != nil { - log.Fatalf("base64 decode webhooks private key: %v", err) - } - privateKey = ed25519.NewKeyFromSeed(seed) - publicKey, _ = privateKey.Public().(ed25519.PublicKey) - - b64fernet, ok := config.Get("sr.ht", "network-key") - if !ok { - log.Fatalf("No network key configured") - } - fernetKey, err = fernet.DecodeKey(b64fernet) - if err != nil { - log.Fatalf("Load Fernet network encryption key: %v", err) - } -} - -func Sign(payload []byte) []byte { - return ed25519.Sign(privateKey, payload) -} - -func Verify(payload, signature []byte) bool { - return ed25519.Verify(publicKey, payload, signature) -} - -func Encrypt(payload []byte) []byte { - msg, err := fernet.EncryptAndSign(payload, fernetKey) - if err != nil { - log.Fatalf("Error encrypting payload: %v", err) - } - return msg -} - -func Decrypt(payload []byte) []byte { - return fernet.VerifyAndDecrypt(payload, - time.Duration(0), []*fernet.Key{fernetKey}) -} diff --git a/api/database/filter.go b/api/database/filter.go deleted file mode 100644 index a935ba1..0000000 --- a/api/database/filter.go +++ /dev/null @@ -1,51 +0,0 @@ -package database - -import ( - "strings" - - "github.com/google/shlex" - sq "github.com/Masterminds/squirrel" -) - -type KeyFunc func(sq.SelectBuilder, string) (string, error) - -type SearchTerm struct { - Key string - Value string - Inverse bool -} - -type Searchable interface { - Selectable - - // Update the select builder for bare search terms - DefaultSearch(sq.SelectBuilder, string) (sq.SelectBuilder, error) - - // Return a map of KeyFuncs for each search key, whose values update the - // select builder for the given search term - //KeySearch() map[string]KeyFunc - - // Update the select builder for a key/value pair which is unknown - //FallbackSearch(sq.SelectBuilder, - // key, value string) (sq.SelectBuilder, error) -} - -func ApplyFilter(query sq.SelectBuilder, resource Searchable, - search string) (sq.SelectBuilder, error) { - terms, err := shlex.Split(search) - if err != nil { - return query, err - } - - for _, term := range terms { - parts := strings.SplitN(term, ":", 2) - if len(parts) == 1 { - query, err = resource.DefaultSearch(query, term) - if err != nil { - return query, err - } - } - } - - return query, nil -} diff --git a/api/database/ql.go b/api/database/ql.go deleted file mode 100644 index 771633d..0000000 --- a/api/database/ql.go +++ /dev/null @@ -1,97 +0,0 @@ -package database - -import ( - "context" - "sort" - - "github.com/lib/pq" - "github.com/vektah/gqlparser/v2/ast" - - "github.com/99designs/gqlgen/graphql" -) - -func collectFields(ctx context.Context) []graphql.CollectedField { - var fields []graphql.CollectedField - if graphql.GetFieldContext(ctx) != nil { - fields = graphql.CollectFieldsCtx(ctx, nil) - - octx := graphql.GetOperationContext(ctx) - for _, col := range fields { - if col.Name == "results" { - // This endpoint is using the cursor pattern; the columns we - // actually need to filter with are nested into the results - // field. - fields = graphql.CollectFields(octx, col.SelectionSet, nil) - break - } - } - } - return fields -} - -func ColumnsFor(ctx context.Context, alias string, - colMap map[string]string) []string { - - fields := collectFields(ctx) - if len(fields) == 0 { - // Collect all fields if we are not in an active graphql context - for qlCol, _ := range colMap { - fields = append(fields, graphql.CollectedField{ - &ast.Field{Name: qlCol}, nil, - }) - } - } - - sort.Slice(fields, func(a, b int) bool { - return fields[a].Name < fields[b].Name - }) - - var columns []string - for _, qlCol := range fields { - if sqlCol, ok := colMap[qlCol.Name]; ok { - if alias != "" { - columns = append(columns, pq.QuoteIdentifier(alias)+ - "."+pq.QuoteIdentifier(sqlCol)) - } else { - columns = append(columns, pq.QuoteIdentifier(sqlCol)) - } - } - } - - return columns -} - -func FieldsFor(ctx context.Context, - colMap map[string]interface{}) []interface{} { - - qlFields := collectFields(ctx) - if len(qlFields) == 0 { - // Collect all fields if we are not in an active graphql context - for qlCol, _ := range colMap { - qlFields = append(qlFields, graphql.CollectedField{ - &ast.Field{Name: qlCol}, nil, - }) - } - } - - sort.Slice(qlFields, func(a, b int) bool { - return qlFields[a].Name < qlFields[b].Name - }) - - var fields []interface{} - for _, qlField := range qlFields { - if field, ok := colMap[qlField.Name]; ok { - fields = append(fields, field) - } - } - - return fields -} - -func WithAlias(alias, col string) string { - if alias != "" { - return alias + "." + col - } else { - return col - } -} diff --git a/api/database/sq.go b/api/database/sq.go deleted file mode 100644 index 22b2558..0000000 --- a/api/database/sq.go +++ /dev/null @@ -1,30 +0,0 @@ -package database - -import ( - "context" - "fmt" - - sq "github.com/Masterminds/squirrel" -) - -type Selectable interface { - Select(ctx context.Context) []string - Fields(ctx context.Context) []interface{} -} - -func Select(ctx context.Context, cols ...interface{}) sq.SelectBuilder { - q := sq.Select().PlaceholderFormat(sq.Dollar) - for _, col := range cols { - switch col := col.(type) { - case string: - q = q.Columns(col) - case []string: - q = q.Columns(col...) - case Selectable: - q = q.Columns(col.Select(ctx)...) - default: - panic(fmt.Errorf("Unknown selectable type %T", col)) - } - } - return q -} diff --git a/api/go.mod b/api/go.mod index 4fec051..1e8bbc2 100644 --- a/api/go.mod +++ b/api/go.mod @@ -4,28 +4,32 @@ go 1.14 require ( git.sr.ht/~sircmpwn/getopt v0.0.0-20191230200459-23622cc906b3 + git.sr.ht/~sircmpwn/gql.sr.ht v0.0.0-20200519155752-5492494ccefa github.com/99designs/gqlgen v0.11.4-0.20200512031635-40570d1b4d70 - github.com/Masterminds/squirrel v1.2.0 + github.com/Masterminds/squirrel v1.4.0 github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect github.com/fernet/fernet-go v0.0.0-20191111064656-eff2850e6001 - github.com/go-chi/chi v3.3.2+incompatible + github.com/go-chi/chi v4.1.1+incompatible github.com/go-git/go-git/v5 v5.0.0 + github.com/golang/protobuf v1.4.2 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/gorilla/websocket v1.4.2 // indirect github.com/hashicorp/golang-lru v0.5.4 // indirect - github.com/lib/pq v1.3.0 + github.com/lib/pq v1.5.2 github.com/martinlindhe/base36 v1.0.0 github.com/matryer/moq v0.0.0-20200310130814-7721994d1b54 // indirect github.com/mitchellh/mapstructure v1.3.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_golang v1.6.0 // indirect + github.com/prometheus/common v0.10.0 // indirect github.com/urfave/cli/v2 v2.2.0 // indirect github.com/vaughan0/go-ini v0.0.0-20130923145212-a98ad7ee00ec github.com/vektah/dataloaden v0.3.0 // indirect github.com/vektah/gqlparser v1.3.1 github.com/vektah/gqlparser/v2 v2.0.1 golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 - golang.org/x/tools v0.0.0-20200513201620-d5fe73897c97 // indirect + golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 // indirect + gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/mail.v2 v2.3.1 gopkg.in/yaml.v2 v2.3.0 // indirect ) diff --git a/api/go.sum b/api/go.sum index e9c6934..9146a70 100644 --- a/api/go.sum +++ b/api/go.sum @@ -4,6 +4,12 @@ git.sr.ht/~sircmpwn/git.sr.ht v0.0.0-20200405134845-b8fbf5bf484f h1:SW8+xV65kcga git.sr.ht/~sircmpwn/git.sr.ht v0.0.0-20200413150414-046cd382d7b7 h1:PYRTIcsHR5W+aPn98OCC73ly528uw5o/4Z3b5Rvc7vA= git.sr.ht/~sircmpwn/git.sr.ht v0.0.0-20200430231646-4014870a700b h1:xHyh0xixoMog7F1kqOG55daFfK0uso98i0lQ/D4dSM8= git.sr.ht/~sircmpwn/git.sr.ht v0.0.0-20200511133609-1161d017dc00 h1:uHwXamWDLQX54bOBJcGub96xOX6ApQtQWJ83QmE39OM= +git.sr.ht/~sircmpwn/gql.sr.ht v0.0.0-20200519142817-97b51d504448 h1:fnNlKiQ/bV8EHlHwWwUQ/icu+ec0MtEgSuwleCUIM40= +git.sr.ht/~sircmpwn/gql.sr.ht v0.0.0-20200519142817-97b51d504448/go.mod h1:V38DHc2+k1GmbNT4pE6OH/2fOXYcZHn9o/PUAEXH1P0= +git.sr.ht/~sircmpwn/gql.sr.ht v0.0.0-20200519153250-9929f08a73ba h1:Sar7LLCjGDyP1J/MmnNyPKIlpnZBV9f5ULLi5aSck8M= +git.sr.ht/~sircmpwn/gql.sr.ht v0.0.0-20200519153250-9929f08a73ba/go.mod h1:V38DHc2+k1GmbNT4pE6OH/2fOXYcZHn9o/PUAEXH1P0= +git.sr.ht/~sircmpwn/gql.sr.ht v0.0.0-20200519155752-5492494ccefa h1:yh3hxZcollhjHZHEjkbh9PrBBFFNClYh0b5SAk+zJAY= +git.sr.ht/~sircmpwn/gql.sr.ht v0.0.0-20200519155752-5492494ccefa/go.mod h1:V38DHc2+k1GmbNT4pE6OH/2fOXYcZHn9o/PUAEXH1P0= git.sr.ht/~sircmpwn/gqlgen v0.0.0-20200412134447-57d7234737d4 h1:J/Sb88htNHzZaN6ZEF8BnRWj3LzYoTrOL4WRhZEEiQE= git.sr.ht/~sircmpwn/gqlgen v0.0.0-20200412134447-57d7234737d4/go.mod h1:W1cijL2EqAyL1eo1WAJ3ijNVkZM2okpYyCF5TRu1VfI= github.com/99designs/gqlgen v0.11.3 h1:oFSxl1DFS9X///uHV3y6CEfpcXWrDUxVblR4Xib2bs4= @@ -13,6 +19,8 @@ github.com/99designs/gqlgen v0.11.4-0.20200512031635-40570d1b4d70/go.mod h1:RgX5 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/Masterminds/squirrel v1.2.0 h1:K1NhbTO21BWG47IVR0OnIZuE0LZcXAYqywrC3Ko53KI= github.com/Masterminds/squirrel v1.2.0/go.mod h1:yaPeOnPG5ZRwL9oKdTsO/prlkPbXWZlRVMQ/gGlzIuA= +github.com/Masterminds/squirrel v1.4.0 h1:he5i/EXixZxrBUWcxzDYMiju9WZ3ld/l7QBNuo/eN3w= +github.com/Masterminds/squirrel v1.4.0/go.mod h1:yaPeOnPG5ZRwL9oKdTsO/prlkPbXWZlRVMQ/gGlzIuA= github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= github.com/agnivade/levenshtein v1.0.3 h1:M5ZnqLOoZR8ygVq0FfkXsNOKzMCk0xRiow0R5+5VkQ0= github.com/agnivade/levenshtein v1.0.3/go.mod h1:4SFRZbbXWLF4MU1T9Qg0pGgH3Pjs+t6ie5efyrwRJXs= @@ -47,6 +55,8 @@ github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/go-chi/chi v3.3.2+incompatible h1:uQNcQN3NsV1j4ANsPh42P4ew4t6rnRbJb8frvpp31qQ= github.com/go-chi/chi v3.3.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= +github.com/go-chi/chi v4.1.1+incompatible h1:MmTgB0R8Bt/jccxp+t6S/1VGIKdJw5J74CK/c9tTfA4= +github.com/go-chi/chi v4.1.1+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4= github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E= github.com/go-git/go-billy/v5 v5.0.0 h1:7NQHvd9FVid8VL4qVUMm8XifBK+2xCoZ2lSk0agRrHM= @@ -72,6 +82,8 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0 h1:oOuy+ugB+P/kBdUnG5QaMXSIyJ1q38wWSojYCb3z5VQ= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= @@ -108,6 +120,8 @@ github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhR github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU= github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.5.2 h1:yTSXVswvWUOQ3k1sd7vJfDrbSl8lKuscqFJRqjC0ifw= +github.com/lib/pq v1.5.2/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= github.com/martinlindhe/base36 v1.0.0 h1:eYsumTah144C0A8P1T/AVSUk5ZoLnhfYFM3OGQxB52A= github.com/martinlindhe/base36 v1.0.0/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8= @@ -153,6 +167,8 @@ github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6T github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.9.1 h1:KOMtN28tlbam3/7ZKEYKHhKoJZYYj3gMH4uc62x7X7U= github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/common v0.10.0 h1:RyRA7RzGXQZiW+tGMr7sxa85G1z0yOpM1qq5c8lNawc= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.11 h1:DhHlBtkHWPYi8O2y31JkK0TF+DGM+51OopZjH/Ia5qI= @@ -201,6 +217,7 @@ golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -226,6 +243,8 @@ golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 h1:uYVVQ9WP/Ds2ROhcaGPeIdVq0 golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f h1:gWF768j/LaZugp8dyS4UwsslYCYz9XgFxvlgsn0n9H8= golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 h1:DYfZAGf2WMFjMxbgTjaC+2HC7NkNAQs+6Q8b9WEB/F4= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -238,6 +257,7 @@ golang.org/x/tools v0.0.0-20200410194907-79a7a3126eef h1:RHORRhs540cYZYrzgU2CPUy golang.org/x/tools v0.0.0-20200410194907-79a7a3126eef/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200513201620-d5fe73897c97 h1:DAuln/hGp+aJiHpID1Y1hYzMEPP5WLwtZHPb50mN0OE= golang.org/x/tools v0.0.0-20200513201620-d5fe73897c97/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200519015757-0d0afa43d58a/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= @@ -248,7 +268,11 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0 h1:qdOKuR/EIArgaWNjetjgTzgVTAZ+S/WXVrq9HW9zimw= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/api/graph/model/acl.go b/api/graph/model/acl.go index 6f81898..5fc64e9 100644 --- a/api/graph/model/acl.go +++ b/api/graph/model/acl.go @@ -8,7 +8,7 @@ import ( sq "github.com/Masterminds/squirrel" - "git.sr.ht/~sircmpwn/git.sr.ht/api/database" + "git.sr.ht/~sircmpwn/gql.sr.ht/database" ) // TODO: Drop updated column from database diff --git a/api/graph/model/cursor.go b/api/graph/model/cursor.go index e36d0ff..10b8d3f 100644 --- a/api/graph/model/cursor.go +++ b/api/graph/model/cursor.go @@ -5,7 +5,7 @@ import ( "fmt" "io" - "git.sr.ht/~sircmpwn/git.sr.ht/api/crypto" + "git.sr.ht/~sircmpwn/gql.sr.ht/crypto" ) // TODO: Add a field for the resource this is intended to be used with diff --git a/api/graph/model/repository.go b/api/graph/model/repository.go index 5fd7571..0b030a8 100644 --- a/api/graph/model/repository.go +++ b/api/graph/model/repository.go @@ -9,7 +9,7 @@ import ( "github.com/go-git/go-git/v5" sq "github.com/Masterminds/squirrel" - "git.sr.ht/~sircmpwn/git.sr.ht/api/database" + "git.sr.ht/~sircmpwn/gql.sr.ht/database" ) type Repository struct { diff --git a/api/graph/model/user.go b/api/graph/model/user.go index 6aff2ce..dd7226f 100644 --- a/api/graph/model/user.go +++ b/api/graph/model/user.go @@ -4,7 +4,7 @@ import ( "context" "time" - "git.sr.ht/~sircmpwn/git.sr.ht/api/database" + "git.sr.ht/~sircmpwn/gql.sr.ht/database" ) type User struct { diff --git a/api/graph/recover.go b/api/graph/recover.go deleted file mode 100644 index 0f8b363..0000000 --- a/api/graph/recover.go +++ /dev/null @@ -1,127 +0,0 @@ -package graph - -import ( - "bytes" - "context" - "crypto/rand" - "encoding/binary" - "errors" - "fmt" - "log" - "net/mail" - "os" - "runtime" - "strconv" - "time" - - "github.com/99designs/gqlgen/graphql" - "github.com/martinlindhe/base36" - "github.com/vaughan0/go-ini" - gomail "gopkg.in/mail.v2" - - "git.sr.ht/~sircmpwn/git.sr.ht/api/auth" -) - -// Provides a graphql.RecoverFunc which will print the stack trace, and if -// debug mode is not enabled, email it to the administrator. -func EmailRecover(config ini.File, debug bool) graphql.RecoverFunc { - return func (ctx context.Context, _origErr interface{}) error { - var ( - ok bool - origErr error - ) - if origErr, ok = _origErr.(error); !ok { - log.Printf("Unexpected error in recover: %v\n", origErr) - return fmt.Errorf("internal system error") - } - - if errors.Is(origErr, context.Canceled) { - return origErr - } - - if errors.Is(origErr, context.DeadlineExceeded) { - return origErr - } - - if origErr.Error() == "pq: canceling statement due to user request" { - return origErr - } - - stack := make([]byte, 32768) // 32 KiB - i := runtime.Stack(stack, false) - log.Println(string(stack[:i])) - if debug { - return fmt.Errorf("internal system error") - } - - to, ok := config.Get("mail", "error-to") - if !ok { - return fmt.Errorf("internal system error") - } - from, _ := config.Get("mail", "error-from") - portStr, ok := config.Get("mail", "smtp-port") - if !ok { - return fmt.Errorf("internal system error") - } - port, _ := strconv.Atoi(portStr) - host, _ := config.Get("mail", "smtp-host") - user, _ := config.Get("mail", "smtp-user") - pass, _ := config.Get("mail", "smtp-password") - - m := gomail.NewMessage() - sender, err := mail.ParseAddress(from) - if err != nil { - log.Fatalf("Failed to parse sender address") - } - m.SetAddressHeader("From", sender.Address, sender.Name) - recipient, err := mail.ParseAddress(to) - if err != nil { - log.Fatalf("Failed to parse recipient address") - } - m.SetAddressHeader("To", recipient.Address, recipient.Name) - m.SetHeader("Message-ID", GenerateMessageID()) - m.SetHeader("Subject", fmt.Sprintf( - "[git.sr.ht] GraphQL query error: %v", origErr)) - - quser := auth.ForContext(ctx) - octx := graphql.GetOperationContext(ctx) - - m.SetBody("text/plain", fmt.Sprintf(`Error occured processing GraphQL request: - - %v - - When running the following query on behalf of %s <%s>: - - %s - - The following stack trace was produced: - - %s`, origErr, quser.Username, quser.Email, octx.RawQuery, string(stack[:i]))) - - d := gomail.NewDialer(host, port, user, pass) - if err := d.DialAndSend(m); err != nil { - log.Printf("Error sending email: %v\n", err) - } - return fmt.Errorf("internal system error") - } -} - -// Generates an RFC 2822-compliant Message-Id based on the informational draft -// "Recommendations for generating Message IDs", for lack of a better -// authoritative source. -func GenerateMessageID() string { - var ( - now bytes.Buffer - nonce []byte = make([]byte, 8) - ) - binary.Write(&now, binary.BigEndian, time.Now().UnixNano()) - rand.Read(nonce) - hostname, err := os.Hostname() - if err != nil { - hostname = "localhost" - } - return fmt.Sprintf("<%s.%s@%s>", - base36.EncodeBytes(now.Bytes()), - base36.EncodeBytes(nonce), - hostname) -} diff --git a/api/graph/resolver.go b/api/graph/resolver.go index a975f3f..9b48c95 100644 --- a/api/graph/resolver.go +++ b/api/graph/resolver.go @@ -2,10 +2,4 @@ package graph //go:generate go run github.com/99designs/gqlgen -import ( - "database/sql" -) - -type Resolver struct { - DB *sql.DB -} +type Resolver struct {} diff --git a/api/graph/schema.resolvers.go b/api/graph/schema.resolvers.go index f68f7b1..9cf3f72 100644 --- a/api/graph/schema.resolvers.go +++ b/api/graph/schema.resolvers.go @@ -9,8 +9,8 @@ import ( "sort" "strings" - "git.sr.ht/~sircmpwn/git.sr.ht/api/auth" - "git.sr.ht/~sircmpwn/git.sr.ht/api/database" + "git.sr.ht/~sircmpwn/gql.sr.ht/auth" + "git.sr.ht/~sircmpwn/gql.sr.ht/database" "git.sr.ht/~sircmpwn/git.sr.ht/api/graph/api" "git.sr.ht/~sircmpwn/git.sr.ht/api/graph/model" "git.sr.ht/~sircmpwn/git.sr.ht/api/loaders" @@ -31,7 +31,7 @@ func (r *aCLResolver) Repository(ctx context.Context, obj *model.ACL) (*model.Re From(`repository repo`). Join(`access acl ON acl.repo_id = repo.id`). Where(`acl.id = ?`, obj.ID) - row := query.RunWith(r.DB).QueryRow() + row := query.RunWith(database.ForContext(ctx)).QueryRow() if err := row.Scan(repo.Fields(ctx)...); err != nil { panic(err) } @@ -48,7 +48,7 @@ func (r *aCLResolver) Entity(ctx context.Context, obj *model.ACL) (model.Entity, From(`"user" u`). Join(`access acl ON acl.user_id = u.id`). Where(`acl.id = ?`, obj.ID) - row := query.RunWith(r.DB).QueryRow() + row := query.RunWith(database.ForContext(ctx)).QueryRow() if err := row.Scan(user.Fields(ctx)...); err != nil { panic(err) } @@ -125,7 +125,7 @@ func (r *queryResolver) Repositories(ctx context.Context, cursor *model.Cursor, From(`repository repo`). Where(`repo.owner_id = ?`, auth.ForContext(ctx).ID) - repos, cursor := repo.QueryWithCursor(ctx, r.DB, query, cursor) + repos, cursor := repo.QueryWithCursor(ctx, database.ForContext(ctx), query, cursor) return &model.RepositoryCursor{repos, cursor}, nil } @@ -164,7 +164,7 @@ func (r *repositoryResolver) AccessControlList(ctx context.Context, obj *model.R Where(`acl.repo_id = ?`, obj.ID). Where(`repo.owner_id = ?`, auth.ForContext(ctx).ID) - acls, cursor := acl.QueryWithCursor(ctx, r.DB, query, cursor) + acls, cursor := acl.QueryWithCursor(ctx, database.ForContext(ctx), query, cursor) return &model.ACLCursor{acls, cursor}, nil } @@ -367,7 +367,7 @@ func (r *userResolver) Repositories(ctx context.Context, obj *model.User, cursor From(`repository repo`). Where(`repo.owner_id = ?`, obj.ID) - repos, cursor := repo.QueryWithCursor(ctx, r.DB, query, cursor) + repos, cursor := repo.QueryWithCursor(ctx, database.ForContext(ctx), query, cursor) return &model.RepositoryCursor{repos, cursor}, nil } diff --git a/api/loaders/middleware.go b/api/loaders/middleware.go index 758e4ff..ea8c157 100644 --- a/api/loaders/middleware.go +++ b/api/loaders/middleware.go @@ -16,8 +16,8 @@ import ( sq "github.com/Masterminds/squirrel" "github.com/lib/pq" - "git.sr.ht/~sircmpwn/git.sr.ht/api/auth" - "git.sr.ht/~sircmpwn/git.sr.ht/api/database" + "git.sr.ht/~sircmpwn/gql.sr.ht/auth" + "git.sr.ht/~sircmpwn/gql.sr.ht/database" "git.sr.ht/~sircmpwn/git.sr.ht/api/graph/model" ) @@ -258,40 +258,39 @@ func fetchRepositoriesByOwnerRepoName(ctx context.Context, } } -func Middleware(db *sql.DB) func(http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ctx := context.WithValue(r.Context(), loadersCtxKey, &Loaders{ - UsersByID: UsersByIDLoader{ - maxBatch: 100, - wait: 1 * time.Millisecond, - fetch: fetchUsersByID(r.Context(), db), - }, - UsersByName: UsersByNameLoader{ - maxBatch: 100, - wait: 1 * time.Millisecond, - fetch: fetchUsersByName(r.Context(), db), - }, - RepositoriesByID: RepositoriesByIDLoader{ - maxBatch: 100, - wait: 1 * time.Millisecond, - fetch: fetchRepositoriesByID(r.Context(), db), - }, - RepositoriesByName: RepositoriesByNameLoader{ - maxBatch: 100, - wait: 1 * time.Millisecond, - fetch: fetchRepositoriesByName(r.Context(), db), - }, - RepositoriesByOwnerRepoName: RepositoriesByOwnerRepoNameLoader{ - maxBatch: 100, - wait: 1 * time.Millisecond, - fetch: fetchRepositoriesByOwnerRepoName(r.Context(), db), - }, - }) - r = r.WithContext(ctx) - next.ServeHTTP(w, r) +func Middleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + db := database.ForContext(r.Context()) + ctx := context.WithValue(r.Context(), loadersCtxKey, &Loaders{ + UsersByID: UsersByIDLoader{ + maxBatch: 100, + wait: 1 * time.Millisecond, + fetch: fetchUsersByID(r.Context(), db), + }, + UsersByName: UsersByNameLoader{ + maxBatch: 100, + wait: 1 * time.Millisecond, + fetch: fetchUsersByName(r.Context(), db), + }, + RepositoriesByID: RepositoriesByIDLoader{ + maxBatch: 100, + wait: 1 * time.Millisecond, + fetch: fetchRepositoriesByID(r.Context(), db), + }, + RepositoriesByName: RepositoriesByNameLoader{ + maxBatch: 100, + wait: 1 * time.Millisecond, + fetch: fetchRepositoriesByName(r.Context(), db), + }, + RepositoriesByOwnerRepoName: RepositoriesByOwnerRepoNameLoader{ + maxBatch: 100, + wait: 1 * time.Millisecond, + fetch: fetchRepositoriesByOwnerRepoName(r.Context(), db), + }, }) - } + r = r.WithContext(ctx) + next.ServeHTTP(w, r) + }) } func ForContext(ctx context.Context) *Loaders { diff --git a/api/server.go b/api/server.go index 0ccc049..40152f5 100644 --- a/api/server.go +++ b/api/server.go @@ -1,140 +1,20 @@ package main import ( - "database/sql" - "log" - "net/http" - "os" - "strconv" - "time" + "git.sr.ht/~sircmpwn/gql.sr.ht" - "git.sr.ht/~sircmpwn/getopt" - "github.com/99designs/gqlgen/graphql/playground" - "github.com/99designs/gqlgen/handler" - "github.com/go-chi/chi" - "github.com/go-chi/chi/middleware" - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promauto" - "github.com/prometheus/client_golang/prometheus/promhttp" - "github.com/vaughan0/go-ini" - _ "github.com/lib/pq" - - "git.sr.ht/~sircmpwn/git.sr.ht/api/auth" - "git.sr.ht/~sircmpwn/git.sr.ht/api/crypto" "git.sr.ht/~sircmpwn/git.sr.ht/api/graph" "git.sr.ht/~sircmpwn/git.sr.ht/api/graph/api" "git.sr.ht/~sircmpwn/git.sr.ht/api/loaders" ) -const defaultAddr = ":5101" - -var ( - requestsProcessed = promauto.NewCounter(prometheus.CounterOpts{ - Name: "git_api_requests_processed_total", - Help: "Total number of API requests processed", - }) - requestDuration = promauto.NewHistogram(prometheus.HistogramOpts{ - Name: "git_api_request_duration_millis", - Help: "Duration of processed HTTP requests in milliseconds", - Buckets: []float64{10, 20, 40, 80, 120, 300, 600, 900, 1800}, - }) -) - func main() { - var ( - addr string = defaultAddr - config ini.File - debug bool - err error - ) - opts, _, err := getopt.Getopts(os.Args, "b:d") - if err != nil { - panic(err) - } - for _, opt := range opts { - switch opt.Option { - case 'b': - addr = opt.Value - case 'd': - debug = true - } - } - - for _, path := range []string{"../config.ini", "/etc/sr.ht/config.ini"} { - config, err = ini.LoadFile(path) - if err == nil { - break - } - } - if err != nil { - log.Fatalf("Failed to load config file: %v", err) - } - - crypto.InitCrypto(config) - - pgcs, ok := config.Get("git.sr.ht", "connection-string") - if !ok { - log.Fatalf("No connection string configured for git.sr.ht: %v", err) - } + appConfig := gql.LoadConfig(":5101") - db, err := sql.Open("postgres", pgcs) - if err != nil { - log.Fatalf("Failed to open a database connection: %v", err) - } - - var timeout time.Duration - if to, ok := config.Get("git.sr.ht::api", "max-duration"); ok { - timeout, err = time.ParseDuration(to) - if err != nil { - panic(err) - } - } else { - timeout = 3 * time.Second - } - - router := chi.NewRouter() - router.Use(func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - start := time.Now() - next.ServeHTTP(w, r) - end := time.Now() - elapsed := end.Sub(start) - requestDuration.Observe(float64(elapsed.Milliseconds())) - requestsProcessed.Inc() - }) - }) - router.Use(auth.Middleware(db)) - router.Use(loaders.Middleware(db)) - router.Use(middleware.Logger) - router.Use(middleware.Timeout(timeout)) - - gqlConfig := api.Config{ - Resolvers: &graph.Resolver{DB: db}, - } + gqlConfig := api.Config{Resolvers: &graph.Resolver{}} graph.ApplyComplexity(&gqlConfig) + schema := api.NewExecutableSchema(gqlConfig) - var complexity int - if limit, ok := config.Get("git.sr.ht::api", "max-complexity"); ok { - complexity, err = strconv.Atoi(limit) - if err != nil { - panic(err) - } - } else { - complexity = 100 - } - - srv := handler.GraphQL( - api.NewExecutableSchema(gqlConfig), - handler.ComplexityLimit(complexity), - handler.RecoverFunc(graph.EmailRecover(config, debug))) - - router.Handle("/query", srv) - router.Handle("/query/metrics", promhttp.Handler()) - - if debug { - router.Handle("/", playground.Handler("GraphQL playground", "/query")) - } - - log.Printf("running on %s", addr) - log.Fatal(http.ListenAndServe(addr, router)) + router := gql.MakeRouter("git.sr.ht", appConfig, schema, loaders.Middleware) + gql.ListenAndServe(router) } -- 2.38.4