D api/auth/auth.go => api/auth/auth.go +0 -260
@@ 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 <token>'`, 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
-}
D api/crypto/crypto.go => api/crypto/crypto.go +0 -60
@@ 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})
-}
D api/database/filter.go => api/database/filter.go +0 -51
@@ 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
-}
D api/database/ql.go => api/database/ql.go +0 -97
@@ 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
- }
-}
D api/database/sq.go => api/database/sq.go +0 -30
@@ 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
-}
M api/go.mod => api/go.mod +8 -4
@@ 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
)
M api/go.sum => api/go.sum +24 -0
@@ 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=
M api/graph/model/acl.go => api/graph/model/acl.go +1 -1
@@ 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
M api/graph/model/cursor.go => api/graph/model/cursor.go +1 -1
@@ 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
M api/graph/model/repository.go => api/graph/model/repository.go +1 -1
@@ 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 {
M api/graph/model/user.go => api/graph/model/user.go +1 -1
@@ 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 {
D api/graph/recover.go => api/graph/recover.go +0 -127
@@ 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)
-}
M api/graph/resolver.go => api/graph/resolver.go +1 -7
@@ 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 {}
M api/graph/schema.resolvers.go => api/graph/schema.resolvers.go +7 -7
@@ 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
}
M api/loaders/middleware.go => api/loaders/middleware.go +34 -35
@@ 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 {
M api/server.go => api/server.go +6 -126
@@ 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)
}