-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathhandler.go
More file actions
180 lines (155 loc) · 5.26 KB
/
handler.go
File metadata and controls
180 lines (155 loc) · 5.26 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
package admin
import (
"crypto/subtle"
"encoding/json"
"log/slog"
"net/http"
"strings"
"github.com/graphql-go/graphql"
"github.com/GainForest/hypergoat/internal/database/repositories"
"github.com/GainForest/hypergoat/internal/oauth"
)
// Handler handles admin GraphQL requests with authentication.
type Handler struct {
schema *graphql.Schema
resolver *Resolver
middleware *oauth.AuthMiddleware
configRepo *repositories.ConfigRepository
adminAPIKey string // shared secret; when set, X-User-DID is trusted if Bearer token matches
}
// NewHandler creates a new admin GraphQL handler.
// When adminAPIKey is non-empty, the X-User-DID header is trusted only if the
// request also carries a matching Authorization: Bearer <key> header.
func NewHandler(repos *Repositories, middleware *oauth.AuthMiddleware, configRepo *repositories.ConfigRepository, domainDID string, adminAPIKey string) (*Handler, error) {
resolver := NewResolver(repos, domainDID)
builder := NewSchemaBuilder(resolver)
schema, err := builder.Build()
if err != nil {
return nil, err
}
return &Handler{
schema: schema,
resolver: resolver,
middleware: middleware,
configRepo: configRepo,
adminAPIKey: adminAPIKey,
}, nil
}
// ServeHTTP handles admin GraphQL HTTP requests.
// CORS is handled by the router-level middleware; not duplicated here.
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Parse the request
var params struct {
Query string `json:"query"`
OperationName string `json:"operationName"`
Variables map[string]interface{} `json:"variables"`
}
if r.Method == "GET" {
params.Query = r.URL.Query().Get("query")
params.OperationName = r.URL.Query().Get("operationName")
} else {
if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
}
// Log mutation requests
if strings.Contains(params.Query, "mutation") {
slog.Info("[admin] Mutation request", "operation", params.OperationName, "variables", params.Variables)
}
// Get authentication info from context (set by middleware) or X-User-DID header
ctx := r.Context()
userDID := oauth.UserIDFromContext(ctx)
// Trust X-User-DID header only when the request carries a valid admin API key.
// This allows frontends and CLI tools to authenticate as a specific user
// without requiring the full OAuth flow.
if userDID == "" && h.adminAPIKey != "" {
if h.validAPIKey(r) {
userDID = r.Header.Get("X-User-DID")
if userDID != "" {
slog.Info("[admin] Auth via X-User-DID + API key",
"did", userDID,
"remote_addr", r.RemoteAddr)
}
} else if r.Header.Get("X-User-DID") != "" {
slog.Warn("[admin] X-User-DID header rejected: missing or invalid API key",
"remote_addr", r.RemoteAddr)
}
}
handle := "" // Would need to resolve from DID
// Get admin DIDs from config
adminDidsStr, err := h.configRepo.Get(ctx, "admin_dids")
if err != nil {
slog.Warn("Failed to get admin DIDs", "error", err)
adminDidsStr = ""
}
var adminDIDs []string
if adminDidsStr != "" {
adminDIDs = strings.Split(adminDidsStr, ",")
for i := range adminDIDs {
adminDIDs[i] = strings.TrimSpace(adminDIDs[i])
}
}
// Check if user is admin
isAdmin := false
for _, adminDID := range adminDIDs {
if adminDID == userDID {
isAdmin = true
break
}
}
// Debug logging for auth
if userDID != "" {
slog.Info("[admin] Authenticated request", "userDID", userDID, "isAdmin", isAdmin)
}
// Inject auth info into context
ctx = ContextWithAuth(ctx, userDID, handle, isAdmin, adminDIDs)
// Execute the query
result := graphql.Do(graphql.Params{
Schema: *h.schema,
RequestString: params.Query,
OperationName: params.OperationName,
VariableValues: params.Variables,
Context: ctx,
})
// Write response
w.Header().Set("Content-Type", "application/json")
if len(result.Errors) > 0 {
// Log errors for debugging
for _, err := range result.Errors {
slog.Debug("GraphQL error", "error", err.Message, "path", err.Path)
}
w.WriteHeader(http.StatusBadRequest)
}
_ = json.NewEncoder(w).Encode(result)
}
// Schema returns the underlying GraphQL schema.
func (h *Handler) Schema() *graphql.Schema {
return h.schema
}
// Resolver returns the admin resolver.
func (h *Handler) Resolver() *Resolver {
return h.resolver
}
// RequireAuth returns a middleware-wrapped handler that requires authentication.
func (h *Handler) RequireAuth() http.Handler {
return h.middleware.RequireAuth(h)
}
// OptionalAuth returns a middleware-wrapped handler that allows optional authentication.
func (h *Handler) OptionalAuth() http.Handler {
return h.middleware.OptionalAuth(h)
}
// validAPIKey checks whether the request carries a valid admin API key.
// Returns true if no API key is configured (backwards-compatible) or if the
// request's Authorization: Bearer token matches the configured key.
func (h *Handler) validAPIKey(r *http.Request) bool {
if h.adminAPIKey == "" {
return true // no key configured — allow (backwards-compatible)
}
auth := r.Header.Get("Authorization")
if !strings.HasPrefix(auth, "Bearer ") {
return false
}
token := strings.TrimPrefix(auth, "Bearer ")
return subtle.ConstantTimeCompare([]byte(token), []byte(h.adminAPIKey)) == 1
}