Skip to content

Commit 0824445

Browse files
gcmsgclaude
andcommitted
feat: add user auth, agent playground, provider console, and community features (Phase 7)
Implement the complete C-side marketplace functionality: - User authentication system with JWT (register/login/refresh/logout/API keys) - Agent invocation endpoint with SSE streaming support and rate limiting - Provider console for publishing, managing, and analyzing agents - Community features: reviews/ratings, categories, abuse reports, trusted badge - Full React frontend: auth UI, playground chat, provider dashboard, publish wizard, review section, category filters, report dialog Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a9c5fed commit 0824445

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

68 files changed

+7548
-27
lines changed

cmd/peerclawd/main.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ import (
2525
"github.com/peerclaw/peerclaw-server/internal/router"
2626
"github.com/peerclaw/peerclaw-server/internal/server"
2727
"github.com/peerclaw/peerclaw-server/internal/signaling"
28+
"github.com/peerclaw/peerclaw-server/internal/invocation"
29+
"github.com/peerclaw/peerclaw-server/internal/review"
30+
"github.com/peerclaw/peerclaw-server/internal/userauth"
2831
"github.com/peerclaw/peerclaw-server/internal/verification"
2932
goredis "github.com/redis/go-redis/v9"
3033
)
@@ -109,6 +112,62 @@ func main() {
109112
logger.Info("endpoint verification initialized")
110113
}
111114

115+
// Initialize user authentication.
116+
var userAuthService *userauth.Service
117+
if cfg.UserAuth.Enabled && sqlDB != nil {
118+
uaStore := userauth.NewStore(cfg.Database.Driver, sqlDB)
119+
if err := uaStore.Migrate(context.Background()); err != nil {
120+
logger.Error("failed to migrate userauth tables", "error", err)
121+
os.Exit(1)
122+
}
123+
124+
jwtSecret := cfg.UserAuth.JWTSecret
125+
if jwtSecret == "" {
126+
jwtSecret = "peerclaw-dev-secret-change-me"
127+
logger.Warn("using default JWT secret — set user_auth.jwt_secret in config for production")
128+
}
129+
130+
accessTTL, err := time.ParseDuration(cfg.UserAuth.AccessTTL)
131+
if err != nil {
132+
accessTTL = 15 * time.Minute
133+
}
134+
refreshTTL, err := time.ParseDuration(cfg.UserAuth.RefreshTTL)
135+
if err != nil {
136+
refreshTTL = 168 * time.Hour
137+
}
138+
139+
jwtMgr := userauth.NewJWTManager(jwtSecret, accessTTL, refreshTTL)
140+
userAuthService = userauth.NewService(uaStore, jwtMgr, cfg.UserAuth.BcryptCost, logger)
141+
logger.Info("user authentication initialized",
142+
"access_ttl", accessTTL,
143+
"refresh_ttl", refreshTTL,
144+
)
145+
}
146+
147+
// Initialize invocation store.
148+
var invocationService *invocation.Service
149+
if sqlDB != nil {
150+
invStore := invocation.NewStore(cfg.Database.Driver, sqlDB)
151+
if err := invStore.Migrate(context.Background()); err != nil {
152+
logger.Error("failed to migrate invocation tables", "error", err)
153+
os.Exit(1)
154+
}
155+
invocationService = invocation.NewService(invStore, logger)
156+
logger.Info("invocation tracking initialized")
157+
}
158+
159+
// Initialize review service.
160+
var reviewService *review.Service
161+
if sqlDB != nil {
162+
revStore := review.NewStore(cfg.Database.Driver, sqlDB)
163+
if err := revStore.Migrate(context.Background()); err != nil {
164+
logger.Error("failed to migrate review tables", "error", err)
165+
os.Exit(1)
166+
}
167+
reviewService = review.NewService(revStore, repEngine, logger)
168+
logger.Info("review service initialized")
169+
}
170+
112171
// Initialize services.
113172
regService := registry.NewService(store, logger)
114173
routeTable := router.NewTable()
@@ -210,6 +269,20 @@ func main() {
210269
if fedService != nil {
211270
httpServer.SetFederation(fedService)
212271
}
272+
if userAuthService != nil {
273+
httpServer.SetUserAuth(userAuthService)
274+
}
275+
if reviewService != nil {
276+
httpServer.SetReviewService(reviewService)
277+
}
278+
if invocationService != nil {
279+
httpServer.SetInvocation(invocationService)
280+
// Anonymous: 10 calls/hour/IP, Authenticated: 100 calls/hour.
281+
invokeRL := server.NewIPRateLimiter(10.0/3600.0, 3)
282+
stopInvokeCleanup := invokeRL.StartCleanup(time.Minute)
283+
defer stopInvokeCleanup()
284+
httpServer.SetInvokeRateLimiter(invokeRL)
285+
}
213286
if sigHub != nil {
214287
sigHub.SetAudit(auditLogger)
215288
sigHub.SetMetrics(otelMetrics)

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,13 @@ require (
2727
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
2828
github.com/go-logr/logr v1.4.3 // indirect
2929
github.com/go-logr/stdr v1.2.2 // indirect
30+
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
3031
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
3132
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
3233
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.41.0 // indirect
3334
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
3435
go.uber.org/atomic v1.11.0 // indirect
36+
golang.org/x/crypto v0.48.0 // indirect
3537
golang.org/x/net v0.50.0 // indirect
3638
golang.org/x/sys v0.41.0 // indirect
3739
golang.org/x/text v0.34.0 // indirect

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
1515
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
1616
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
1717
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
18+
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
19+
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
1820
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
1921
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
2022
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
@@ -69,6 +71,8 @@ go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
6971
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
7072
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
7173
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
74+
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
75+
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
7276
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
7377
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
7478
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=

internal/config/config.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,23 @@ type Config struct {
2121
AuditLog AuditLogConfig `yaml:"audit_log"`
2222
Federation FederationConfig `yaml:"federation"`
2323
Auth AuthConfig `yaml:"auth"`
24+
UserAuth UserAuthConfig `yaml:"user_auth"`
2425
}
2526

2627
// AuthConfig holds authentication settings.
2728
type AuthConfig struct {
2829
Required bool `yaml:"required"` // When true, reject unauthenticated requests. Default false for transition.
2930
}
3031

32+
// UserAuthConfig holds user authentication settings.
33+
type UserAuthConfig struct {
34+
Enabled bool `yaml:"enabled"` // default true
35+
JWTSecret string `yaml:"jwt_secret"` // supports ${ENV_VAR}
36+
AccessTTL string `yaml:"access_ttl"` // default "15m"
37+
RefreshTTL string `yaml:"refresh_ttl"` // default "168h" (7 days)
38+
BcryptCost int `yaml:"bcrypt_cost"` // default 12
39+
}
40+
3141
// ServerConfig holds HTTP and gRPC server settings.
3242
type ServerConfig struct {
3343
HTTPAddr string `yaml:"http_addr"`
@@ -166,6 +176,12 @@ func DefaultConfig() *Config {
166176
Federation: FederationConfig{
167177
Enabled: false,
168178
},
179+
UserAuth: UserAuthConfig{
180+
Enabled: true,
181+
AccessTTL: "15m",
182+
RefreshTTL: "168h",
183+
BcryptCost: 12,
184+
},
169185
}
170186
}
171187

@@ -200,6 +216,7 @@ func (c *Config) resolveSecrets() {
200216
c.Database.DSN = resolveEnv(c.Database.DSN)
201217
c.Signaling.TURN.Credential = resolveEnv(c.Signaling.TURN.Credential)
202218
c.Federation.AuthToken = resolveEnv(c.Federation.AuthToken)
219+
c.UserAuth.JWTSecret = resolveEnv(c.UserAuth.JWTSecret)
203220
for i := range c.Federation.Peers {
204221
c.Federation.Peers[i].Token = resolveEnv(c.Federation.Peers[i].Token)
205222
}

internal/identity/trust.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ func ExtractBearerToken(authHeader string) (string, error) {
6868
type contextKey string
6969

7070
const agentIDKey contextKey = "agent_id"
71+
const userIDKey contextKey = "user_id"
72+
const userRoleKey contextKey = "user_role"
7173

7274
// WithAgentID stores the agent ID in the context.
7375
func WithAgentID(ctx context.Context, agentID string) context.Context {
@@ -79,3 +81,25 @@ func AgentIDFromContext(ctx context.Context) (string, bool) {
7981
id, ok := ctx.Value(agentIDKey).(string)
8082
return id, ok
8183
}
84+
85+
// WithUserID stores the user ID in the context.
86+
func WithUserID(ctx context.Context, userID string) context.Context {
87+
return context.WithValue(ctx, userIDKey, userID)
88+
}
89+
90+
// UserIDFromContext retrieves the user ID from the context.
91+
func UserIDFromContext(ctx context.Context) (string, bool) {
92+
id, ok := ctx.Value(userIDKey).(string)
93+
return id, ok
94+
}
95+
96+
// WithUserRole stores the user role in the context.
97+
func WithUserRole(ctx context.Context, role string) context.Context {
98+
return context.WithValue(ctx, userRoleKey, role)
99+
}
100+
101+
// UserRoleFromContext retrieves the user role from the context.
102+
func UserRoleFromContext(ctx context.Context) (string, bool) {
103+
role, ok := ctx.Value(userRoleKey).(string)
104+
return role, ok
105+
}

internal/invocation/factory.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package invocation
2+
3+
import "database/sql"
4+
5+
// NewStore creates a Store based on the driver name.
6+
func NewStore(driver string, db *sql.DB) Store {
7+
switch driver {
8+
case "postgres":
9+
return NewPostgresStore(db)
10+
default:
11+
return NewSQLiteStore(db)
12+
}
13+
}

0 commit comments

Comments
 (0)