diff --git a/cmd/server/main.go b/cmd/server/main.go index 053585c..a6aa575 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -1,6 +1,14 @@ package main import ( + "Coves/internal/api/handlers/oauth" + "Coves/internal/api/middleware" + "Coves/internal/api/routes" + "Coves/internal/atproto/did" + "Coves/internal/atproto/identity" + "Coves/internal/atproto/jetstream" + "Coves/internal/core/communities" + "Coves/internal/core/users" "bytes" "context" "database/sql" @@ -18,15 +26,8 @@ import ( _ "github.com/lib/pq" "github.com/pressly/goose/v3" - "Coves/internal/api/handlers/oauth" - "Coves/internal/api/middleware" - "Coves/internal/api/routes" - "Coves/internal/atproto/did" - "Coves/internal/atproto/identity" - "Coves/internal/atproto/jetstream" - "Coves/internal/core/communities" oauthCore "Coves/internal/core/oauth" - "Coves/internal/core/users" + postgresRepo "Coves/internal/db/postgres" ) @@ -48,20 +49,24 @@ func main() { if err != nil { log.Fatal("Failed to connect to database:", err) } - defer db.Close() + defer func() { + if closeErr := db.Close(); closeErr != nil { + log.Printf("Failed to close database connection: %v", closeErr) + } + }() - if err := db.Ping(); err != nil { + if err = db.Ping(); err != nil { log.Fatal("Failed to ping database:", err) } log.Println("Connected to AppView database") // Run migrations - if err := goose.SetDialect("postgres"); err != nil { + if err = goose.SetDialect("postgres"); err != nil { log.Fatal("Failed to set goose dialect:", err) } - if err := goose.Up(db, "internal/db/migrations"); err != nil { + if err = goose.Up(db, "internal/db/migrations"); err != nil { log.Fatal("Failed to run migrations:", err) } @@ -84,7 +89,7 @@ func main() { identityConfig.PLCURL = plcURL } if cacheTTL := os.Getenv("IDENTITY_CACHE_TTL"); cacheTTL != "" { - if duration, err := time.ParseDuration(cacheTTL); err == nil { + if duration, parseErr := time.ParseDuration(cacheTTL); parseErr == nil { identityConfig.CacheTTL = duration } } @@ -154,9 +159,9 @@ func main() { pdsPassword := os.Getenv("PDS_INSTANCE_PASSWORD") if pdsHandle != "" && pdsPassword != "" { log.Printf("Authenticating Coves instance (%s) with PDS...", instanceDID) - accessToken, err := authenticateWithPDS(defaultPDS, pdsHandle, pdsPassword) - if err != nil { - log.Printf("Warning: Failed to authenticate with PDS: %v", err) + accessToken, authErr := authenticateWithPDS(defaultPDS, pdsHandle, pdsPassword) + if authErr != nil { + log.Printf("Warning: Failed to authenticate with PDS: %v", authErr) log.Println("Community creation will fail until PDS authentication is configured") } else { if svc, ok := communityService.(interface{ SetPDSAccessToken(string) }); ok { @@ -180,8 +185,8 @@ func main() { userConsumer := jetstream.NewUserEventConsumer(userService, identityResolver, jetstreamURL, pdsFilter) ctx := context.Background() go func() { - if err := userConsumer.Start(ctx); err != nil { - log.Printf("Jetstream consumer stopped: %v", err) + if startErr := userConsumer.Start(ctx); startErr != nil { + log.Printf("Jetstream consumer stopped: %v", startErr) } }() @@ -199,8 +204,12 @@ func main() { defer ticker.Stop() for range ticker.C { if pgStore, ok := sessionStore.(*oauthCore.PostgresSessionStore); ok { - _ = pgStore.CleanupExpiredRequests(ctx) - _ = pgStore.CleanupExpiredSessions(ctx) + if cleanupErr := pgStore.CleanupExpiredRequests(ctx); cleanupErr != nil { + log.Printf("Failed to cleanup expired OAuth requests: %v", cleanupErr) + } + if cleanupErr := pgStore.CleanupExpiredSessions(ctx); cleanupErr != nil { + log.Printf("Failed to cleanup expired OAuth sessions: %v", cleanupErr) + } log.Println("OAuth cleanup completed") } } @@ -242,7 +251,9 @@ func main() { r.Get("/health", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) - w.Write([]byte("OK")) + if _, err := w.Write([]byte("OK")); err != nil { + log.Printf("Failed to write health check response: %v", err) + } }) port := os.Getenv("APPVIEW_PORT") @@ -284,10 +295,17 @@ func authenticateWithPDS(pdsURL, handle, password string) (string, error) { if err != nil { return "", fmt.Errorf("failed to call PDS: %w", err) } - defer resp.Body.Close() + defer func() { + if closeErr := resp.Body.Close(); closeErr != nil { + log.Printf("Failed to close response body: %v", closeErr) + } + }() if resp.StatusCode != http.StatusOK { - body, _ := io.ReadAll(resp.Body) + body, readErr := io.ReadAll(resp.Body) + if readErr != nil { + return "", fmt.Errorf("PDS returned status %d and failed to read body: %w", resp.StatusCode, readErr) + } return "", fmt.Errorf("PDS returned status %d: %s", resp.StatusCode, string(body)) } diff --git a/cmd/validate-lexicon/main.go b/cmd/validate-lexicon/main.go index 3b8fffa..929900f 100644 --- a/cmd/validate-lexicon/main.go +++ b/cmd/validate-lexicon/main.go @@ -88,10 +88,13 @@ func validateSchemaStructure(catalog *lexicon.BaseCatalog, schemaPath string, ve // Only process .json files if !info.IsDir() && filepath.Ext(path) == ".json" { schemaFiles = append(schemaFiles, path) - + // Convert file path to schema ID // e.g., internal/atproto/lexicon/social/coves/actor/profile.json -> social.coves.actor.profile - relPath, _ := filepath.Rel(schemaPath, path) + relPath, err := filepath.Rel(schemaPath, path) + if err != nil { + return fmt.Errorf("failed to compute relative path: %w", err) + } schemaID := filepath.ToSlash(relPath) schemaID = schemaID[:len(schemaID)-5] // Remove .json extension schemaID = strings.ReplaceAll(schemaID, "/", ".") @@ -99,7 +102,6 @@ func validateSchemaStructure(catalog *lexicon.BaseCatalog, schemaPath string, ve } return nil }) - if err != nil { return fmt.Errorf("error walking schema directory: %w", err) } @@ -157,7 +159,6 @@ func loadSchemasWithDebug(catalog *lexicon.BaseCatalog, schemaPath string, verbo } return nil }) - if err != nil { return fmt.Errorf("error walking schema directory: %w", err) } @@ -182,50 +183,55 @@ func loadSchemasWithDebug(catalog *lexicon.BaseCatalog, schemaPath string, verbo // extractAllSchemaIDs walks the schema directory and returns all schema IDs func extractAllSchemaIDs(schemaPath string) []string { var schemaIDs []string - - filepath.Walk(schemaPath, func(path string, info os.FileInfo, err error) error { + + if err := filepath.Walk(schemaPath, func(path string, info os.FileInfo, err error) error { if err != nil { return err } - + // Skip test-data directory if info.IsDir() && info.Name() == "test-data" { return filepath.SkipDir } - + // Only process .json files if !info.IsDir() && filepath.Ext(path) == ".json" { // Convert file path to schema ID - relPath, _ := filepath.Rel(schemaPath, path) + relPath, err := filepath.Rel(schemaPath, path) + if err != nil { + return err + } schemaID := filepath.ToSlash(relPath) schemaID = schemaID[:len(schemaID)-5] // Remove .json extension schemaID = strings.ReplaceAll(schemaID, "/", ".") - + // Only include record schemas (not procedures) - if strings.Contains(schemaID, ".record") || - strings.Contains(schemaID, ".profile") || - strings.Contains(schemaID, ".rules") || - strings.Contains(schemaID, ".wiki") || - strings.Contains(schemaID, ".subscription") || - strings.Contains(schemaID, ".membership") || - strings.Contains(schemaID, ".vote") || - strings.Contains(schemaID, ".tag") || - strings.Contains(schemaID, ".comment") || - strings.Contains(schemaID, ".share") || - strings.Contains(schemaID, ".tribunalVote") || - strings.Contains(schemaID, ".ruleProposal") || - strings.Contains(schemaID, ".ban") { + if strings.Contains(schemaID, ".record") || + strings.Contains(schemaID, ".profile") || + strings.Contains(schemaID, ".rules") || + strings.Contains(schemaID, ".wiki") || + strings.Contains(schemaID, ".subscription") || + strings.Contains(schemaID, ".membership") || + strings.Contains(schemaID, ".vote") || + strings.Contains(schemaID, ".tag") || + strings.Contains(schemaID, ".comment") || + strings.Contains(schemaID, ".share") || + strings.Contains(schemaID, ".tribunalVote") || + strings.Contains(schemaID, ".ruleProposal") || + strings.Contains(schemaID, ".ban") { schemaIDs = append(schemaIDs, schemaID) } } return nil - }) - + }); err != nil { + log.Printf("Warning: failed to walk schema directory: %v", err) + } + return schemaIDs } // validateTestData validates test JSON data files against their corresponding schemas -func validateTestData(catalog *lexicon.BaseCatalog, testDataPath string, verbose bool, strict bool, allSchemas []string) error { +func validateTestData(catalog *lexicon.BaseCatalog, testDataPath string, verbose, strict bool, allSchemas []string) error { // Check if test data directory exists if _, err := os.Stat(testDataPath); os.IsNotExist(err) { return fmt.Errorf("test data path does not exist: %s", testDataPath) @@ -248,7 +254,7 @@ func validateTestData(catalog *lexicon.BaseCatalog, testDataPath string, verbose if !info.IsDir() && filepath.Ext(path) == ".json" { filename := filepath.Base(path) isInvalidTest := strings.Contains(filename, "-invalid-") - + if verbose { if isInvalidTest { fmt.Printf("\n Testing (expect failure): %s\n", filename) @@ -263,11 +269,15 @@ func validateTestData(catalog *lexicon.BaseCatalog, testDataPath string, verbose validationErrors = append(validationErrors, fmt.Sprintf("Failed to open %s: %v", path, err)) return nil } - defer file.Close() + defer func() { + if closeErr := file.Close(); closeErr != nil { + validationErrors = append(validationErrors, fmt.Sprintf("Failed to close %s: %v", path, closeErr)) + } + }() - data, err := io.ReadAll(file) - if err != nil { - validationErrors = append(validationErrors, fmt.Sprintf("Failed to read %s: %v", path, err)) + data, readErr := io.ReadAll(file) + if readErr != nil { + validationErrors = append(validationErrors, fmt.Sprintf("Failed to read %s: %v", path, readErr)) return nil } @@ -275,11 +285,11 @@ func validateTestData(catalog *lexicon.BaseCatalog, testDataPath string, verbose var recordData map[string]interface{} decoder := json.NewDecoder(bytes.NewReader(data)) decoder.UseNumber() // This preserves numbers as json.Number instead of float64 - if err := decoder.Decode(&recordData); err != nil { - validationErrors = append(validationErrors, fmt.Sprintf("Failed to parse JSON in %s: %v", path, err)) + if decodeErr := decoder.Decode(&recordData); decodeErr != nil { + validationErrors = append(validationErrors, fmt.Sprintf("Failed to parse JSON in %s: %v", path, decodeErr)) return nil } - + // Convert json.Number values to appropriate types recordData = convertNumbers(recordData).(map[string]interface{}) @@ -299,15 +309,15 @@ func validateTestData(catalog *lexicon.BaseCatalog, testDataPath string, verbose } // Validate the record - err = lexicon.ValidateRecord(catalog, recordData, recordType, flags) - + validateErr := lexicon.ValidateRecord(catalog, recordData, recordType, flags) + if isInvalidTest { // This file should fail validation invalidFiles++ - if err != nil { + if validateErr != nil { invalidFailCount++ if verbose { - fmt.Printf(" āœ… Correctly rejected invalid %s record: %v\n", recordType, err) + fmt.Printf(" āœ… Correctly rejected invalid %s record: %v\n", recordType, validateErr) } } else { validationErrors = append(validationErrors, fmt.Sprintf("Invalid test file %s passed validation when it should have failed", path)) @@ -318,10 +328,10 @@ func validateTestData(catalog *lexicon.BaseCatalog, testDataPath string, verbose } else { // This file should pass validation validFiles++ - if err != nil { - validationErrors = append(validationErrors, fmt.Sprintf("Validation failed for %s (type: %s): %v", path, recordType, err)) + if validateErr != nil { + validationErrors = append(validationErrors, fmt.Sprintf("Validation failed for %s (type: %s): %v", path, recordType, validateErr)) if verbose { - fmt.Printf(" āŒ Failed: %v\n", err) + fmt.Printf(" āŒ Failed: %v\n", validateErr) } } else { validSuccessCount++ @@ -334,7 +344,6 @@ func validateTestData(catalog *lexicon.BaseCatalog, testDataPath string, verbose } return nil }) - if err != nil { return fmt.Errorf("error walking test data directory: %w", err) } @@ -355,22 +364,22 @@ func validateTestData(catalog *lexicon.BaseCatalog, testDataPath string, verbose fmt.Printf("\nšŸ“‹ Validation Summary:\n") fmt.Printf(" Valid test files: %d/%d passed\n", validSuccessCount, validFiles) fmt.Printf(" Invalid test files: %d/%d correctly rejected\n", invalidFailCount, invalidFiles) - + if validSuccessCount == validFiles && invalidFailCount == invalidFiles { fmt.Printf("\n āœ… All test files behaved as expected!\n") } - + // Show test coverage summary (only for valid files) fmt.Printf("\nšŸ“Š Test Data Coverage Summary:\n") fmt.Printf(" - Records with test data: %d types\n", len(testedTypes)) fmt.Printf(" - Valid test files: %d\n", validFiles) fmt.Printf(" - Invalid test files: %d (for error validation)\n", invalidFiles) - + fmt.Printf("\n Tested record types:\n") for recordType := range testedTypes { fmt.Printf(" āœ“ %s\n", recordType) } - + // Show untested schemas untestedCount := 0 fmt.Printf("\n āš ļø Record types without test data:\n") @@ -380,12 +389,12 @@ func validateTestData(catalog *lexicon.BaseCatalog, testDataPath string, verbose untestedCount++ } } - + if untestedCount == 0 { fmt.Println(" (None - full test coverage!)") } else { - fmt.Printf("\n Coverage: %d/%d record types have test data (%.1f%%)\n", - len(testedTypes), len(allSchemas), + fmt.Printf("\n Coverage: %d/%d record types have test data (%.1f%%)\n", + len(testedTypes), len(allSchemas), float64(len(testedTypes))/float64(len(allSchemas))*100) } } @@ -404,7 +413,7 @@ func validateCrossReferences(catalog *lexicon.BaseCatalog, verbose bool) error { "social.coves.richtext.facet#italic", "social.coves.richtext.facet#strikethrough", "social.coves.richtext.facet#spoiler", - + // Post types and views "social.coves.post.get#postView", "social.coves.post.get#authorView", @@ -414,13 +423,13 @@ func validateCrossReferences(catalog *lexicon.BaseCatalog, verbose bool) error { "social.coves.post.get#externalView", "social.coves.post.get#postStats", "social.coves.post.get#viewerState", - + // Post record types "social.coves.post.record#originalAuthor", - + // Actor definitions "social.coves.actor.profile#geoLocation", - + // Community definitions "social.coves.community.rules#rule", } diff --git a/internal/api/handlers/community/create.go b/internal/api/handlers/community/create.go index 7a6acd2..6d36196 100644 --- a/internal/api/handlers/community/create.go +++ b/internal/api/handlers/community/create.go @@ -1,10 +1,9 @@ package community import ( + "Coves/internal/core/communities" "encoding/json" "net/http" - - "Coves/internal/core/communities" ) // CreateHandler handles community creation @@ -68,5 +67,9 @@ func (h *CreateHandler) HandleCreate(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(response) + if err := json.NewEncoder(w).Encode(response); err != nil { + // Log encoding errors but don't return error response (headers already sent) + // This follows Go's standard practice for HTTP handlers + _ = err + } } diff --git a/internal/api/handlers/community/errors.go b/internal/api/handlers/community/errors.go index dc9a6fa..07fa75d 100644 --- a/internal/api/handlers/community/errors.go +++ b/internal/api/handlers/community/errors.go @@ -1,11 +1,10 @@ package community import ( + "Coves/internal/core/communities" "encoding/json" "log" "net/http" - - "Coves/internal/core/communities" ) // XRPCError represents an XRPC error response @@ -18,10 +17,12 @@ type XRPCError struct { func writeError(w http.ResponseWriter, status int, error, message string) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) - json.NewEncoder(w).Encode(XRPCError{ + if err := json.NewEncoder(w).Encode(XRPCError{ Error: error, Message: message, - }) + }); err != nil { + log.Printf("Failed to encode error response: %v", err) + } } // handleServiceError converts service errors to appropriate HTTP responses diff --git a/internal/api/handlers/community/get.go b/internal/api/handlers/community/get.go index 3e9ed22..7649385 100644 --- a/internal/api/handlers/community/get.go +++ b/internal/api/handlers/community/get.go @@ -1,10 +1,9 @@ package community import ( + "Coves/internal/core/communities" "encoding/json" "net/http" - - "Coves/internal/core/communities" ) // GetHandler handles community retrieval @@ -44,5 +43,9 @@ func (h *GetHandler) HandleGet(w http.ResponseWriter, r *http.Request) { // Return community data w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(community) + if err := json.NewEncoder(w).Encode(community); err != nil { + // Log encoding errors but don't return error response (headers already sent) + // This follows Go's standard practice for HTTP handlers + _ = err + } } diff --git a/internal/api/handlers/community/list.go b/internal/api/handlers/community/list.go index 0686067..cde230a 100644 --- a/internal/api/handlers/community/list.go +++ b/internal/api/handlers/community/list.go @@ -1,11 +1,10 @@ package community import ( + "Coves/internal/core/communities" "encoding/json" "net/http" "strconv" - - "Coves/internal/core/communities" ) // ListHandler handles listing communities @@ -70,5 +69,9 @@ func (h *ListHandler) HandleList(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(response) + if err := json.NewEncoder(w).Encode(response); err != nil { + // Log encoding errors but don't return error response (headers already sent) + // This follows Go's standard practice for HTTP handlers + _ = err + } } diff --git a/internal/api/handlers/community/search.go b/internal/api/handlers/community/search.go index 2a7e32a..3490b0a 100644 --- a/internal/api/handlers/community/search.go +++ b/internal/api/handlers/community/search.go @@ -1,11 +1,10 @@ package community import ( + "Coves/internal/core/communities" "encoding/json" "net/http" "strconv" - - "Coves/internal/core/communities" ) // SearchHandler handles community search @@ -74,5 +73,9 @@ func (h *SearchHandler) HandleSearch(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(response) + if err := json.NewEncoder(w).Encode(response); err != nil { + // Log encoding errors but don't return error response (headers already sent) + // This follows Go's standard practice for HTTP handlers + _ = err + } } diff --git a/internal/api/handlers/community/subscribe.go b/internal/api/handlers/community/subscribe.go index 6b87c2b..4a95eb6 100644 --- a/internal/api/handlers/community/subscribe.go +++ b/internal/api/handlers/community/subscribe.go @@ -1,10 +1,10 @@ package community import ( + "Coves/internal/core/communities" "encoding/json" + "log" "net/http" - - "Coves/internal/core/communities" ) // SubscribeHandler handles community subscriptions @@ -70,7 +70,9 @@ func (h *SubscribeHandler) HandleSubscribe(w http.ResponseWriter, r *http.Reques w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(response) + if err := json.NewEncoder(w).Encode(response); err != nil { + log.Printf("Failed to encode response: %v", err) + } } // HandleUnsubscribe unsubscribes a user from a community @@ -118,7 +120,9 @@ func (h *SubscribeHandler) HandleUnsubscribe(w http.ResponseWriter, r *http.Requ // Return success response w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(map[string]interface{}{ + if err := json.NewEncoder(w).Encode(map[string]interface{}{ "success": true, - }) + }); err != nil { + log.Printf("Failed to encode response: %v", err) + } } diff --git a/internal/api/handlers/oauth/callback.go b/internal/api/handlers/oauth/callback.go index ca94b7e..fd5674e 100644 --- a/internal/api/handlers/oauth/callback.go +++ b/internal/api/handlers/oauth/callback.go @@ -1,13 +1,13 @@ package oauth import ( + "Coves/internal/atproto/oauth" "log" "net/http" "os" "strings" "time" - "Coves/internal/atproto/oauth" oauthCore "Coves/internal/core/oauth" ) @@ -154,8 +154,8 @@ func (h *CallbackHandler) HandleCallback(w http.ResponseWriter, r *http.Request) ExpiresAt: expiresAt, } - if err := h.sessionStore.SaveSession(session); err != nil { - log.Printf("Failed to save OAuth session: %v", err) + if saveErr := h.sessionStore.SaveSession(session); saveErr != nil { + log.Printf("Failed to save OAuth session: %v", saveErr) http.Error(w, "Failed to save session", http.StatusInternalServerError) return } @@ -168,7 +168,12 @@ func (h *CallbackHandler) HandleCallback(w http.ResponseWriter, r *http.Request) if err != nil { log.Printf("Failed to get cookie session: %v", err) // Try to create a new session anyway - httpSession, _ = cookieStore.New(r, sessionName) + httpSession, err = cookieStore.New(r, sessionName) + if err != nil { + log.Printf("Failed to create new session: %v", err) + http.Error(w, "Failed to create session", http.StatusInternalServerError) + return + } } httpSession.Values[sessionDID] = oauthReq.DID diff --git a/internal/api/handlers/oauth/env_test.go b/internal/api/handlers/oauth/env_test.go index 5af3c47..5f978ab 100644 --- a/internal/api/handlers/oauth/env_test.go +++ b/internal/api/handlers/oauth/env_test.go @@ -62,8 +62,14 @@ func TestGetEnvBase64OrPlain(t *testing.T) { t.Run(tt.name, func(t *testing.T) { // Set environment variable if tt.envValue != "" { - os.Setenv(tt.envKey, tt.envValue) - defer os.Unsetenv(tt.envKey) + if err := os.Setenv(tt.envKey, tt.envValue); err != nil { + t.Fatalf("Failed to set env var: %v", err) + } + defer func() { + if err := os.Unsetenv(tt.envKey); err != nil { + t.Errorf("Failed to unset env var: %v", err) + } + }() } got, err := GetEnvBase64OrPlain(tt.envKey) @@ -103,8 +109,14 @@ func TestGetEnvBase64OrPlain_RealWorldJWK(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - os.Setenv("TEST_REAL_JWK", tt.envValue) - defer os.Unsetenv("TEST_REAL_JWK") + if err := os.Setenv("TEST_REAL_JWK", tt.envValue); err != nil { + t.Fatalf("Failed to set env var: %v", err) + } + defer func() { + if err := os.Unsetenv("TEST_REAL_JWK"); err != nil { + t.Errorf("Failed to unset env var: %v", err) + } + }() got, err := GetEnvBase64OrPlain("TEST_REAL_JWK") if err != nil { diff --git a/internal/api/handlers/oauth/jwks.go b/internal/api/handlers/oauth/jwks.go index 7b8f25d..e80ed68 100644 --- a/internal/api/handlers/oauth/jwks.go +++ b/internal/api/handlers/oauth/jwks.go @@ -1,11 +1,11 @@ package oauth import ( + "Coves/internal/atproto/oauth" "encoding/json" + "log" "net/http" - "Coves/internal/atproto/oauth" - "github.com/lestrrat-go/jwx/v2/jwk" ) @@ -47,5 +47,7 @@ func HandleJWKS(w http.ResponseWriter, r *http.Request) { // Serve JWKS w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(jwks) + if err := json.NewEncoder(w).Encode(jwks); err != nil { + log.Printf("Failed to encode JWKS response: %v", err) + } } diff --git a/internal/api/handlers/oauth/login.go b/internal/api/handlers/oauth/login.go index e98e3ca..0724ca0 100644 --- a/internal/api/handlers/oauth/login.go +++ b/internal/api/handlers/oauth/login.go @@ -1,14 +1,14 @@ package oauth import ( + "Coves/internal/atproto/identity" + "Coves/internal/atproto/oauth" "encoding/json" "log" "net/http" "net/url" "strings" - "Coves/internal/atproto/identity" - "Coves/internal/atproto/oauth" oauthCore "Coves/internal/core/oauth" ) @@ -144,8 +144,8 @@ func (h *LoginHandler) HandleLogin(w http.ResponseWriter, r *http.Request) { ReturnURL: req.ReturnURL, } - if err := h.sessionStore.SaveRequest(oauthReq); err != nil { - log.Printf("Failed to save OAuth request: %v", err) + if saveErr := h.sessionStore.SaveRequest(oauthReq); saveErr != nil { + log.Printf("Failed to save OAuth request: %v", saveErr) http.Error(w, "Failed to save authorization state", http.StatusInternalServerError) return } @@ -171,5 +171,7 @@ func (h *LoginHandler) HandleLogin(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(resp) + if err := json.NewEncoder(w).Encode(resp); err != nil { + log.Printf("Failed to encode response: %v", err) + } } diff --git a/internal/api/handlers/oauth/metadata.go b/internal/api/handlers/oauth/metadata.go index 3c7689b..aae7d96 100644 --- a/internal/api/handlers/oauth/metadata.go +++ b/internal/api/handlers/oauth/metadata.go @@ -13,15 +13,15 @@ type ClientMetadata struct { ClientID string `json:"client_id"` ClientName string `json:"client_name"` ClientURI string `json:"client_uri"` - RedirectURIs []string `json:"redirect_uris"` - GrantTypes []string `json:"grant_types"` - ResponseTypes []string `json:"response_types"` Scope string `json:"scope"` TokenEndpointAuthMethod string `json:"token_endpoint_auth_method"` TokenEndpointAuthSigningAlg string `json:"token_endpoint_auth_signing_alg"` - DpopBoundAccessTokens bool `json:"dpop_bound_access_tokens"` ApplicationType string `json:"application_type"` - JwksURI string `json:"jwks_uri,omitempty"` // Only in production + JwksURI string `json:"jwks_uri,omitempty"` + RedirectURIs []string `json:"redirect_uris"` + GrantTypes []string `json:"grant_types"` + ResponseTypes []string `json:"response_types"` + DpopBoundAccessTokens bool `json:"dpop_bound_access_tokens"` } // HandleClientMetadata serves the OAuth client metadata @@ -55,7 +55,11 @@ func HandleClientMetadata(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(metadata) + if err := json.NewEncoder(w).Encode(metadata); err != nil { + // Log encoding errors but don't return error response (headers already sent) + // This follows Go's standard practice for HTTP handlers + _ = err + } } // getAppViewURL returns the public URL of the AppView diff --git a/internal/api/routes/user.go b/internal/api/routes/user.go index 2de306e..e0f942c 100644 --- a/internal/api/routes/user.go +++ b/internal/api/routes/user.go @@ -1,12 +1,13 @@ package routes import ( + "Coves/internal/core/users" "encoding/json" "errors" + "log" "net/http" "time" - "Coves/internal/core/users" "github.com/go-chi/chi/v5" ) @@ -73,7 +74,9 @@ func (h *UserHandler) GetProfile(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(response) + if err := json.NewEncoder(w).Encode(response); err != nil { + log.Printf("Failed to encode response: %v", err) + } } // Signup handles social.coves.actor.signup @@ -106,7 +109,9 @@ func (h *UserHandler) Signup(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(response) + if err := json.NewEncoder(w).Encode(response); err != nil { + log.Printf("Failed to encode response: %v", err) + } } // respondWithLexiconError maps domain errors to lexicon error types and HTTP status codes @@ -168,8 +173,10 @@ func respondWithLexiconError(w http.ResponseWriter, err error) { // XRPC error response format w.Header().Set("Content-Type", "application/json") w.WriteHeader(statusCode) - json.NewEncoder(w).Encode(map[string]interface{}{ + if err := json.NewEncoder(w).Encode(map[string]interface{}{ "error": errorName, "message": message, - }) -} \ No newline at end of file + }); err != nil { + log.Printf("Failed to encode error response: %v", err) + } +} diff --git a/internal/atproto/did/generator.go b/internal/atproto/did/generator.go index bf9180e..c738947 100644 --- a/internal/atproto/did/generator.go +++ b/internal/atproto/did/generator.go @@ -9,8 +9,8 @@ import ( // Generator creates DIDs for Coves entities type Generator struct { - isDevEnv bool // true = generate without registering, false = register with PLC - plcDirectoryURL string // PLC directory URL (only used when isDevEnv=false) + plcDirectoryURL string + isDevEnv bool } // NewGenerator creates a new DID generator @@ -51,10 +51,11 @@ func (g *Generator) GenerateCommunityDID() (string, error) { // 4. Store keypair securely for future DID updates // // For now, we just generate the identifier (works fine for local dev) - if !g.isDevEnv { - // Future: implement PLC registration here - // return "", fmt.Errorf("PLC registration not yet implemented") - } + // Production PLC registration is not yet implemented - DIDs are generated + // locally but not registered with the PLC directory. This is acceptable + // for development and private instances, but production deployments should + // implement full PLC registration to ensure DIDs are globally resolvable. + _ = g.isDevEnv // Acknowledge that isDevEnv will be used when PLC registration is implemented return did, nil } diff --git a/internal/atproto/identity/postgres_cache.go b/internal/atproto/identity/postgres_cache.go index 1c320fa..e54255d 100644 --- a/internal/atproto/identity/postgres_cache.go +++ b/internal/atproto/identity/postgres_cache.go @@ -143,8 +143,8 @@ func (r *postgresCache) Purge(ctx context.Context, identifier string) error { return fmt.Errorf("failed to purge identity cache: %w", err) } - rowsAffected, _ := result.RowsAffected() - if rowsAffected > 0 { + rowsAffected, err := result.RowsAffected() + if err == nil && rowsAffected > 0 { log.Printf("[identity-cache] Purged %d entries for: %s", rowsAffected, identifier) } diff --git a/internal/atproto/jetstream/community_consumer.go b/internal/atproto/jetstream/community_consumer.go index d3583df..c73e323 100644 --- a/internal/atproto/jetstream/community_consumer.go +++ b/internal/atproto/jetstream/community_consumer.go @@ -1,14 +1,12 @@ package jetstream import ( + "Coves/internal/core/communities" "context" "encoding/json" "fmt" "log" - "strings" "time" - - "Coves/internal/core/communities" ) // CommunityEventConsumer consumes community-related events from Jetstream @@ -124,8 +122,8 @@ func (c *CommunityEventConsumer) createCommunity(ctx context.Context, did string // Handle description facets (rich text) if profile.DescriptionFacets != nil { - facetsJSON, err := json.Marshal(profile.DescriptionFacets) - if err == nil { + facetsJSON, marshalErr := json.Marshal(profile.DescriptionFacets) + if marshalErr == nil { community.DescriptionFacets = facetsJSON } } @@ -195,8 +193,8 @@ func (c *CommunityEventConsumer) updateCommunity(ctx context.Context, did string // Update description facets if profile.DescriptionFacets != nil { - facetsJSON, err := json.Marshal(profile.DescriptionFacets) - if err == nil { + facetsJSON, marshalErr := json.Marshal(profile.DescriptionFacets) + if marshalErr == nil { existing.DescriptionFacets = facetsJSON } } @@ -296,27 +294,25 @@ func (c *CommunityEventConsumer) handleUnsubscribe(ctx context.Context, userDID // Helper types and functions type CommunityProfile struct { - // V2 ONLY: No DID field (repo DID is authoritative) - Handle string `json:"handle"` // Scoped handle (!gaming@coves.social) - AtprotoHandle string `json:"atprotoHandle"` // Real atProto handle (gaming.communities.coves.social) - Name string `json:"name"` + CreatedAt time.Time `json:"createdAt"` + Avatar map[string]interface{} `json:"avatar"` + Banner map[string]interface{} `json:"banner"` + CreatedBy string `json:"createdBy"` + Visibility string `json:"visibility"` + AtprotoHandle string `json:"atprotoHandle"` DisplayName string `json:"displayName"` + Name string `json:"name"` + Handle string `json:"handle"` + HostedBy string `json:"hostedBy"` Description string `json:"description"` + FederatedID string `json:"federatedId"` + ModerationType string `json:"moderationType"` + FederatedFrom string `json:"federatedFrom"` + ContentWarnings []string `json:"contentWarnings"` DescriptionFacets []interface{} `json:"descriptionFacets"` - Avatar map[string]interface{} `json:"avatar"` - Banner map[string]interface{} `json:"banner"` - // Owner field removed - V2 communities ALWAYS self-own (owner == repo DID) - CreatedBy string `json:"createdBy"` - HostedBy string `json:"hostedBy"` - Visibility string `json:"visibility"` - Federation FederationConfig `json:"federation"` - ModerationType string `json:"moderationType"` - ContentWarnings []string `json:"contentWarnings"` - MemberCount int `json:"memberCount"` - SubscriberCount int `json:"subscriberCount"` - FederatedFrom string `json:"federatedFrom"` - FederatedID string `json:"federatedId"` - CreatedAt time.Time `json:"createdAt"` + MemberCount int `json:"memberCount"` + SubscriberCount int `json:"subscriberCount"` + Federation FederationConfig `json:"federation"` } type FederationConfig struct { @@ -365,17 +361,3 @@ func extractBlobCID(blob map[string]interface{}) (string, bool) { return link, true } - -// validateHandle checks if a handle matches expected format (!name@instance) -func validateHandle(handle string) bool { - if !strings.HasPrefix(handle, "!") { - return false - } - - parts := strings.Split(handle, "@") - if len(parts) != 2 { - return false - } - - return true -} diff --git a/internal/atproto/jetstream/user_consumer.go b/internal/atproto/jetstream/user_consumer.go index 240fb9b..8a1017f 100644 --- a/internal/atproto/jetstream/user_consumer.go +++ b/internal/atproto/jetstream/user_consumer.go @@ -1,40 +1,40 @@ package jetstream import ( + "Coves/internal/atproto/identity" + "Coves/internal/core/users" "context" "encoding/json" "fmt" "log" "time" - "Coves/internal/atproto/identity" - "Coves/internal/core/users" "github.com/gorilla/websocket" ) // JetstreamEvent represents an event from the Jetstream firehose // Jetstream documentation: https://docs.bsky.app/docs/advanced-guides/jetstream type JetstreamEvent struct { - Did string `json:"did"` - TimeUS int64 `json:"time_us"` - Kind string `json:"kind"` // "account", "commit", "identity" Account *AccountEvent `json:"account,omitempty"` Identity *IdentityEvent `json:"identity,omitempty"` Commit *CommitEvent `json:"commit,omitempty"` + Did string `json:"did"` + Kind string `json:"kind"` + TimeUS int64 `json:"time_us"` } type AccountEvent struct { - Active bool `json:"active"` Did string `json:"did"` - Seq int64 `json:"seq"` Time string `json:"time"` + Seq int64 `json:"seq"` + Active bool `json:"active"` } type IdentityEvent struct { Did string `json:"did"` Handle string `json:"handle"` - Seq int64 `json:"seq"` Time string `json:"time"` + Seq int64 `json:"seq"` } // CommitEvent represents a record commit from Jetstream @@ -56,7 +56,7 @@ type UserEventConsumer struct { } // NewUserEventConsumer creates a new Jetstream consumer for user events -func NewUserEventConsumer(userService users.UserService, identityResolver identity.Resolver, wsURL string, pdsFilter string) *UserEventConsumer { +func NewUserEventConsumer(userService users.UserService, identityResolver identity.Resolver, wsURL, pdsFilter string) *UserEventConsumer { return &UserEventConsumer{ userService: userService, identityResolver: identityResolver, @@ -91,16 +91,24 @@ func (c *UserEventConsumer) connect(ctx context.Context) error { if err != nil { return fmt.Errorf("failed to connect to Jetstream: %w", err) } - defer conn.Close() + defer func() { + if err := conn.Close(); err != nil { + log.Printf("Failed to close WebSocket connection: %v", err) + } + }() log.Println("Connected to Jetstream") // Set read deadline to detect connection issues - conn.SetReadDeadline(time.Now().Add(60 * time.Second)) + if err := conn.SetReadDeadline(time.Now().Add(60 * time.Second)); err != nil { + log.Printf("Failed to set read deadline: %v", err) + } // Set pong handler to keep connection alive conn.SetPongHandler(func(string) error { - conn.SetReadDeadline(time.Now().Add(60 * time.Second)) + if err := conn.SetReadDeadline(time.Now().Add(60 * time.Second)); err != nil { + log.Printf("Failed to set read deadline in pong handler: %v", err) + } return nil }) @@ -146,7 +154,9 @@ func (c *UserEventConsumer) connect(ctx context.Context) error { } // Reset read deadline on successful read - conn.SetReadDeadline(time.Now().Add(60 * time.Second)) + if err := conn.SetReadDeadline(time.Now().Add(60 * time.Second)); err != nil { + log.Printf("Failed to set read deadline: %v", err) + } if err := c.handleEvent(ctx, message); err != nil { log.Printf("Error handling event: %v", err) diff --git a/internal/atproto/oauth/dpop.go b/internal/atproto/oauth/dpop.go index 741de59..c6e74b3 100644 --- a/internal/atproto/oauth/dpop.go +++ b/internal/atproto/oauth/dpop.go @@ -92,15 +92,15 @@ func CreateDPoPProof(privateKey jwk.Key, method, uri, nonce, accessToken string) // Create headers with DPoP-specific fields // RFC 9449 requires the "jwk" header to contain the public key as a JSON object headers := jws.NewHeaders() - if err := headers.Set(jws.AlgorithmKey, jwa.ES256); err != nil { - return "", fmt.Errorf("failed to set algorithm: %w", err) + if setErr := headers.Set(jws.AlgorithmKey, jwa.ES256); setErr != nil { + return "", fmt.Errorf("failed to set algorithm: %w", setErr) } - if err := headers.Set(jws.TypeKey, "dpop+jwt"); err != nil { - return "", fmt.Errorf("failed to set type: %w", err) + if setErr := headers.Set(jws.TypeKey, "dpop+jwt"); setErr != nil { + return "", fmt.Errorf("failed to set type: %w", setErr) } // Set the public JWK directly - jwx library will handle serialization - if err := headers.Set(jws.JWKKey, pubKey); err != nil { - return "", fmt.Errorf("failed to set JWK: %w", err) + if setErr := headers.Set(jws.JWKKey, pubKey); setErr != nil { + return "", fmt.Errorf("failed to set JWK: %w", setErr) } // Sign using jws.Sign to preserve custom headers diff --git a/internal/atproto/oauth/dpop_test.go b/internal/atproto/oauth/dpop_test.go index a68d908..14af595 100644 --- a/internal/atproto/oauth/dpop_test.go +++ b/internal/atproto/oauth/dpop_test.go @@ -28,14 +28,14 @@ func TestCreateDPoPProof(t *testing.T) { } // Decode and inspect the header - headerJSON, err := base64.RawURLEncoding.DecodeString(parts[0]) - if err != nil { - t.Fatalf("Failed to decode header: %v", err) + headerJSON, decodeErr := base64.RawURLEncoding.DecodeString(parts[0]) + if decodeErr != nil { + t.Fatalf("Failed to decode header: %v", decodeErr) } var header map[string]interface{} - if err := json.Unmarshal(headerJSON, &header); err != nil { - t.Fatalf("Failed to unmarshal header: %v", err) + if unmarshalErr := json.Unmarshal(headerJSON, &header); unmarshalErr != nil { + t.Fatalf("Failed to unmarshal header: %v", unmarshalErr) } t.Logf("DPoP Header: %s", string(headerJSON)) @@ -122,9 +122,14 @@ func TestDPoPProofWithNonce(t *testing.T) { // Decode payload parts := strings.Split(proof, ".") - payloadJSON, _ := base64.RawURLEncoding.DecodeString(parts[1]) + payloadJSON, err := base64.RawURLEncoding.DecodeString(parts[1]) + if err != nil { + t.Fatalf("Failed to decode payload: %v", err) + } var payload map[string]interface{} - json.Unmarshal(payloadJSON, &payload) + if err := json.Unmarshal(payloadJSON, &payload); err != nil { + t.Fatalf("Failed to unmarshal payload: %v", err) + } if payload["nonce"] != testNonce { t.Errorf("Expected nonce=%s, got %v", testNonce, payload["nonce"]) @@ -146,9 +151,14 @@ func TestDPoPProofWithAccessToken(t *testing.T) { // Decode payload parts := strings.Split(proof, ".") - payloadJSON, _ := base64.RawURLEncoding.DecodeString(parts[1]) + payloadJSON, err := base64.RawURLEncoding.DecodeString(parts[1]) + if err != nil { + t.Fatalf("Failed to decode payload: %v", err) + } var payload map[string]interface{} - json.Unmarshal(payloadJSON, &payload) + if err := json.Unmarshal(payloadJSON, &payload); err != nil { + t.Fatalf("Failed to unmarshal payload: %v", err) + } ath, hasATH := payload["ath"] if !hasATH { diff --git a/internal/atproto/xrpc/dpop_transport.go b/internal/atproto/xrpc/dpop_transport.go index 8ae57c0..b23fa30 100644 --- a/internal/atproto/xrpc/dpop_transport.go +++ b/internal/atproto/xrpc/dpop_transport.go @@ -1,11 +1,12 @@ package xrpc import ( + "Coves/internal/atproto/oauth" "fmt" + "log" "net/http" "sync" - "Coves/internal/atproto/oauth" oauthCore "Coves/internal/core/oauth" "github.com/lestrrat-go/jwx/v2/jwk" @@ -18,11 +19,11 @@ import ( // 3. Handles nonce rotation (retries on 401 with new nonce) // 4. Updates nonces in session store type DPoPTransport struct { - base http.RoundTripper // Underlying transport (usually http.DefaultTransport) - session *oauthCore.OAuthSession // User's OAuth session - sessionStore oauthCore.SessionStore // For updating nonces - dpopKey jwk.Key // Parsed DPoP private key - mu sync.Mutex // Protects nonce updates + base http.RoundTripper // Underlying transport (usually http.DefaultTransport) + session *oauthCore.OAuthSession // User's OAuth session + sessionStore oauthCore.SessionStore // For updating nonces + dpopKey jwk.Key // Parsed DPoP private key + mu sync.Mutex // Protects nonce updates } // NewDPoPTransport creates a new DPoP-enabled HTTP transport @@ -87,7 +88,9 @@ func (t *DPoPTransport) RoundTrip(req *http.Request) (*http.Response, error) { t.updateDPoPNonce(req.URL.String(), newNonce) // Close the 401 response body - _ = resp.Body.Close() + if err := resp.Body.Close(); err != nil { + log.Printf("Failed to close response body: %v", err) + } // Retry with new nonce return t.retryWithNewNonce(req, newNonce) @@ -134,7 +137,9 @@ func (t *DPoPTransport) updateDPoPNonce(url, newNonce string) { t.mu.Unlock() // Persist to database (async, best-effort) go func() { - _ = t.sessionStore.UpdatePDSNonce(did, newNonce) + if err := t.sessionStore.UpdatePDSNonce(did, newNonce); err != nil { + log.Printf("Failed to update PDS nonce: %v", err) + } }() return } @@ -145,7 +150,9 @@ func (t *DPoPTransport) updateDPoPNonce(url, newNonce string) { t.mu.Unlock() // Persist to database (async, best-effort) go func() { - _ = t.sessionStore.UpdateAuthServerNonce(did, newNonce) + if err := t.sessionStore.UpdateAuthServerNonce(did, newNonce); err != nil { + log.Printf("Failed to update auth server nonce: %v", err) + } }() return } diff --git a/internal/core/communities/service.go b/internal/core/communities/service.go index f6e24f7..8e6d8f1 100644 --- a/internal/core/communities/service.go +++ b/internal/core/communities/service.go @@ -1,17 +1,17 @@ package communities import ( + "Coves/internal/atproto/did" "bytes" "context" "encoding/json" "fmt" "io" + "log" "net/http" "regexp" "strings" "time" - - "Coves/internal/atproto/did" ) // Community handle validation regex (DNS-valid handle: name.communities.instance.com) @@ -21,15 +21,15 @@ var communityHandleRegex = regexp.MustCompile(`^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[ type communityService struct { repo Repository didGen *did.Generator - pdsURL string // PDS URL for write-forward operations - instanceDID string // DID of this Coves instance - instanceDomain string // Domain of this instance (for handles) - pdsAccessToken string // Access token for authenticating to PDS as the instance - provisioner *PDSAccountProvisioner // V2: Creates PDS accounts for communities + provisioner *PDSAccountProvisioner + pdsURL string + instanceDID string + instanceDomain string + pdsAccessToken string } // NewCommunityService creates a new community service -func NewCommunityService(repo Repository, didGen *did.Generator, pdsURL string, instanceDID string, instanceDomain string, provisioner *PDSAccountProvisioner) Service { +func NewCommunityService(repo Repository, didGen *did.Generator, pdsURL, instanceDID, instanceDomain string, provisioner *PDSAccountProvisioner) Service { return &communityService{ repo: repo, didGen: didGen, @@ -81,8 +81,8 @@ func (s *communityService) CreateCommunity(ctx context.Context, req CreateCommun } // Validate the atProto handle - if err := s.ValidateHandle(pdsAccount.Handle); err != nil { - return nil, fmt.Errorf("generated atProto handle is invalid: %w", err) + if validateErr := s.ValidateHandle(pdsAccount.Handle); validateErr != nil { + return nil, fmt.Errorf("generated atProto handle is invalid: %w", validateErr) } // Build community profile record @@ -580,19 +580,6 @@ func (s *communityService) createRecordOnPDSAs(ctx context.Context, repoDID, col return s.callPDSWithAuth(ctx, "POST", endpoint, payload, accessToken) } -func (s *communityService) putRecordOnPDS(ctx context.Context, repoDID, collection, rkey string, record map[string]interface{}) (string, string, error) { - endpoint := fmt.Sprintf("%s/xrpc/com.atproto.repo.putRecord", strings.TrimSuffix(s.pdsURL, "/")) - - payload := map[string]interface{}{ - "repo": repoDID, - "collection": collection, - "rkey": rkey, - "record": record, - } - - return s.callPDS(ctx, "POST", endpoint, payload) -} - // putRecordOnPDSAs updates a record with a specific access token (for V2 community auth) func (s *communityService) putRecordOnPDSAs(ctx context.Context, repoDID, collection, rkey string, record map[string]interface{}, accessToken string) (string, string, error) { endpoint := fmt.Sprintf("%s/xrpc/com.atproto.repo.putRecord", strings.TrimSuffix(s.pdsURL, "/")) @@ -660,7 +647,11 @@ func (s *communityService) callPDSWithAuth(ctx context.Context, method, endpoint if err != nil { return "", "", fmt.Errorf("failed to call PDS: %w", err) } - defer resp.Body.Close() + defer func() { + if closeErr := resp.Body.Close(); closeErr != nil { + log.Printf("Failed to close response body: %v", closeErr) + } + }() body, err := io.ReadAll(resp.Body) if err != nil { @@ -689,28 +680,6 @@ func (s *communityService) callPDSWithAuth(ctx context.Context, method, endpoint // Helper functions -func extractDomain(didOrURL string) string { - // For did:web:example.com -> example.com - if strings.HasPrefix(didOrURL, "did:web:") { - parts := strings.Split(didOrURL, ":") - if len(parts) >= 3 { - return parts[2] - } - } - - // For URLs, extract domain - if strings.Contains(didOrURL, "://") { - parts := strings.Split(didOrURL, "://") - if len(parts) >= 2 { - domain := strings.Split(parts[1], "/")[0] - domain = strings.Split(domain, ":")[0] // Remove port - return domain - } - } - - return "" -} - func extractRKeyFromURI(uri string) string { // at://did/collection/rkey -> rkey parts := strings.Split(uri, "/") diff --git a/internal/core/oauth/auth_service.go b/internal/core/oauth/auth_service.go index f825ec6..8980801 100644 --- a/internal/core/oauth/auth_service.go +++ b/internal/core/oauth/auth_service.go @@ -1,12 +1,11 @@ package oauth import ( + "Coves/internal/atproto/oauth" "context" "fmt" "time" - "Coves/internal/atproto/oauth" - "github.com/lestrrat-go/jwx/v2/jwk" ) @@ -71,8 +70,10 @@ func (s *AuthService) RefreshTokenIfNeeded(ctx context.Context, session *OAuthSe // Update nonce if provided (best effort - non-critical) if tokenResp.DpopAuthserverNonce != "" { session.DPoPAuthServerNonce = tokenResp.DpopAuthserverNonce - if err := s.sessionStore.UpdateAuthServerNonce(session.DID, tokenResp.DpopAuthserverNonce); err != nil { + if updateErr := s.sessionStore.UpdateAuthServerNonce(session.DID, tokenResp.DpopAuthserverNonce); updateErr != nil { // Log but don't fail - nonce will be updated on next request + // (We ignore the error here intentionally as nonce updates are non-critical) + _ = updateErr } } diff --git a/internal/core/users/service.go b/internal/core/users/service.go index 9b88f4b..b8ec49b 100644 --- a/internal/core/users/service.go +++ b/internal/core/users/service.go @@ -1,17 +1,17 @@ package users import ( + "Coves/internal/atproto/identity" "bytes" "context" "encoding/json" "fmt" "io" + "log" "net/http" "regexp" "strings" "time" - - "Coves/internal/atproto/identity" ) // atProto handle validation regex (per official atProto spec: https://atproto.com/specs/handle) @@ -187,7 +187,11 @@ func (s *userService) RegisterAccount(ctx context.Context, req RegisterAccountRe if err != nil { return nil, fmt.Errorf("failed to call PDS: %w", err) } - defer resp.Body.Close() + defer func() { + if closeErr := resp.Body.Close(); closeErr != nil { + log.Printf("Failed to close response body: %v", closeErr) + } + }() body, err := io.ReadAll(resp.Body) if err != nil { @@ -293,4 +297,4 @@ func validateHandle(handle string) error { } return nil -} \ No newline at end of file +} diff --git a/internal/db/postgres/community_repo.go b/internal/db/postgres/community_repo.go index 620c7c9..832229a 100644 --- a/internal/db/postgres/community_repo.go +++ b/internal/db/postgres/community_repo.go @@ -1,12 +1,13 @@ package postgres import ( + "Coves/internal/core/communities" "context" "database/sql" "fmt" + "log" "strings" - "Coves/internal/core/communities" "github.com/lib/pq" ) @@ -44,7 +45,7 @@ func (r *postgresCommunityRepo) Create(ctx context.Context, community *communiti // Handle JSONB field - use sql.NullString with valid JSON or NULL var descFacets interface{} - if community.DescriptionFacets != nil && len(community.DescriptionFacets) > 0 { + if len(community.DescriptionFacets) > 0 { descFacets = community.DescriptionFacets } else { descFacets = nil @@ -82,7 +83,6 @@ func (r *postgresCommunityRepo) Create(ctx context.Context, community *communiti nullString(community.RecordURI), nullString(community.RecordCID), ).Scan(&community.ID, &community.CreatedAt, &community.UpdatedAt) - if err != nil { // Check for unique constraint violations if strings.Contains(err.Error(), "duplicate key") { @@ -240,7 +240,7 @@ func (r *postgresCommunityRepo) Update(ctx context.Context, community *communiti // Handle JSONB field - use sql.NullString with valid JSON or NULL var descFacets interface{} - if community.DescriptionFacets != nil && len(community.DescriptionFacets) > 0 { + if len(community.DescriptionFacets) > 0 { descFacets = community.DescriptionFacets } else { descFacets = nil @@ -358,7 +358,11 @@ func (r *postgresCommunityRepo) List(ctx context.Context, req communities.ListCo if err != nil { return nil, 0, fmt.Errorf("failed to list communities: %w", err) } - defer rows.Close() + defer func() { + if closeErr := rows.Close(); closeErr != nil { + log.Printf("Failed to close rows: %v", closeErr) + } + }() result := []*communities.Community{} for rows.Next() { @@ -368,7 +372,7 @@ func (r *postgresCommunityRepo) List(ctx context.Context, req communities.ListCo var descFacets []byte var contentWarnings []string - err := rows.Scan( + scanErr := rows.Scan( &community.ID, &community.DID, &community.Handle, &community.Name, &displayName, &description, &descFacets, &avatarCID, &bannerCID, @@ -380,8 +384,8 @@ func (r *postgresCommunityRepo) List(ctx context.Context, req communities.ListCo &community.CreatedAt, &community.UpdatedAt, &recordURI, &recordCID, ) - if err != nil { - return nil, 0, fmt.Errorf("failed to scan community: %w", err) + if scanErr != nil { + return nil, 0, fmt.Errorf("failed to scan community: %w", scanErr) } // Map nullable fields @@ -456,7 +460,11 @@ func (r *postgresCommunityRepo) Search(ctx context.Context, req communities.Sear if err != nil { return nil, 0, fmt.Errorf("failed to search communities: %w", err) } - defer rows.Close() + defer func() { + if closeErr := rows.Close(); closeErr != nil { + log.Printf("Failed to close rows: %v", closeErr) + } + }() result := []*communities.Community{} for rows.Next() { @@ -467,7 +475,7 @@ func (r *postgresCommunityRepo) Search(ctx context.Context, req communities.Sear var contentWarnings []string var relevance float64 - err := rows.Scan( + scanErr := rows.Scan( &community.ID, &community.DID, &community.Handle, &community.Name, &displayName, &description, &descFacets, &avatarCID, &bannerCID, @@ -480,8 +488,8 @@ func (r *postgresCommunityRepo) Search(ctx context.Context, req communities.Sear &recordURI, &recordCID, &relevance, ) - if err != nil { - return nil, 0, fmt.Errorf("failed to scan community: %w", err) + if scanErr != nil { + return nil, 0, fmt.Errorf("failed to scan community: %w", scanErr) } // Map nullable fields @@ -513,10 +521,3 @@ func (r *postgresCommunityRepo) Search(ctx context.Context, req communities.Sear func nullString(s string) sql.NullString { return sql.NullString{String: s, Valid: s != ""} } - -func nullBytes(b []byte) []byte { - if b == nil || len(b) == 0 { - return nil - } - return b -} diff --git a/internal/db/postgres/community_repo_memberships.go b/internal/db/postgres/community_repo_memberships.go index 56912d1..562c9db 100644 --- a/internal/db/postgres/community_repo_memberships.go +++ b/internal/db/postgres/community_repo_memberships.go @@ -1,12 +1,12 @@ package postgres import ( + "Coves/internal/core/communities" "context" "database/sql" "fmt" + "log" "strings" - - "Coves/internal/core/communities" ) // CreateMembership creates a new membership record @@ -29,7 +29,6 @@ func (r *postgresCommunityRepo) CreateMembership(ctx context.Context, membership membership.IsBanned, membership.IsModerator, ).Scan(&membership.ID, &membership.JoinedAt, &membership.LastActiveAt) - if err != nil { if strings.Contains(err.Error(), "duplicate key") { return nil, fmt.Errorf("membership already exists") @@ -120,13 +119,17 @@ func (r *postgresCommunityRepo) ListMembers(ctx context.Context, communityDID st if err != nil { return nil, fmt.Errorf("failed to list members: %w", err) } - defer rows.Close() + defer func() { + if closeErr := rows.Close(); closeErr != nil { + log.Printf("Failed to close rows: %v", closeErr) + } + }() result := []*communities.Membership{} for rows.Next() { membership := &communities.Membership{} - err := rows.Scan( + scanErr := rows.Scan( &membership.ID, &membership.UserDID, &membership.CommunityDID, @@ -137,8 +140,8 @@ func (r *postgresCommunityRepo) ListMembers(ctx context.Context, communityDID st &membership.IsBanned, &membership.IsModerator, ) - if err != nil { - return nil, fmt.Errorf("failed to scan member: %w", err) + if scanErr != nil { + return nil, fmt.Errorf("failed to scan member: %w", scanErr) } result = append(result, membership) @@ -169,7 +172,6 @@ func (r *postgresCommunityRepo) CreateModerationAction(ctx context.Context, acti action.CreatedAt, action.ExpiresAt, ).Scan(&action.ID, &action.CreatedAt) - if err != nil { if strings.Contains(err.Error(), "foreign key") { return nil, communities.ErrCommunityNotFound @@ -193,14 +195,18 @@ func (r *postgresCommunityRepo) ListModerationActions(ctx context.Context, commu if err != nil { return nil, fmt.Errorf("failed to list moderation actions: %w", err) } - defer rows.Close() + defer func() { + if closeErr := rows.Close(); closeErr != nil { + log.Printf("Failed to close rows: %v", closeErr) + } + }() result := []*communities.ModerationAction{} for rows.Next() { action := &communities.ModerationAction{} var reason sql.NullString - err := rows.Scan( + scanErr := rows.Scan( &action.ID, &action.CommunityDID, &action.Action, @@ -210,8 +216,8 @@ func (r *postgresCommunityRepo) ListModerationActions(ctx context.Context, commu &action.CreatedAt, &action.ExpiresAt, ) - if err != nil { - return nil, fmt.Errorf("failed to scan moderation action: %w", err) + if scanErr != nil { + return nil, fmt.Errorf("failed to scan moderation action: %w", scanErr) } action.Reason = reason.String diff --git a/internal/db/postgres/community_repo_subscriptions.go b/internal/db/postgres/community_repo_subscriptions.go index b073c78..461ce99 100644 --- a/internal/db/postgres/community_repo_subscriptions.go +++ b/internal/db/postgres/community_repo_subscriptions.go @@ -1,12 +1,12 @@ package postgres import ( + "Coves/internal/core/communities" "context" "database/sql" "fmt" + "log" "strings" - - "Coves/internal/core/communities" ) // Subscribe creates a new subscription record @@ -23,7 +23,6 @@ func (r *postgresCommunityRepo) Subscribe(ctx context.Context, subscription *com nullString(subscription.RecordURI), nullString(subscription.RecordCID), ).Scan(&subscription.ID, &subscription.SubscribedAt) - if err != nil { if strings.Contains(err.Error(), "duplicate key") { return nil, communities.ErrSubscriptionAlreadyExists @@ -44,7 +43,11 @@ func (r *postgresCommunityRepo) SubscribeWithCount(ctx context.Context, subscrip if err != nil { return nil, fmt.Errorf("failed to begin transaction: %w", err) } - defer tx.Rollback() + defer func() { + if rollbackErr := tx.Rollback(); rollbackErr != nil && rollbackErr != sql.ErrTxDone { + log.Printf("Failed to rollback transaction: %v", rollbackErr) + } + }() // Insert subscription with ON CONFLICT DO NOTHING for idempotency query := ` @@ -70,8 +73,8 @@ func (r *postgresCommunityRepo) SubscribeWithCount(ctx context.Context, subscrip return nil, fmt.Errorf("failed to get existing subscription: %w", err) } // Don't increment count - subscription already existed - if err := tx.Commit(); err != nil { - return nil, fmt.Errorf("failed to commit transaction: %w", err) + if commitErr := tx.Commit(); commitErr != nil { + return nil, fmt.Errorf("failed to commit transaction: %w", commitErr) } return subscription, nil } @@ -129,7 +132,11 @@ func (r *postgresCommunityRepo) UnsubscribeWithCount(ctx context.Context, userDI if err != nil { return fmt.Errorf("failed to begin transaction: %w", err) } - defer tx.Rollback() + defer func() { + if rollbackErr := tx.Rollback(); rollbackErr != nil && rollbackErr != sql.ErrTxDone { + log.Printf("Failed to rollback transaction: %v", rollbackErr) + } + }() // Delete subscription deleteQuery := `DELETE FROM community_subscriptions WHERE user_did = $1 AND community_did = $2` @@ -145,8 +152,8 @@ func (r *postgresCommunityRepo) UnsubscribeWithCount(ctx context.Context, userDI // If no rows deleted, subscription didn't exist (idempotent - not an error) if rowsAffected == 0 { - if err := tx.Commit(); err != nil { - return fmt.Errorf("failed to commit transaction: %w", err) + if commitErr := tx.Commit(); commitErr != nil { + return fmt.Errorf("failed to commit transaction: %w", commitErr) } return nil } @@ -214,14 +221,18 @@ func (r *postgresCommunityRepo) ListSubscriptions(ctx context.Context, userDID s if err != nil { return nil, fmt.Errorf("failed to list subscriptions: %w", err) } - defer rows.Close() + defer func() { + if closeErr := rows.Close(); closeErr != nil { + log.Printf("Failed to close rows: %v", closeErr) + } + }() result := []*communities.Subscription{} for rows.Next() { subscription := &communities.Subscription{} var recordURI, recordCID sql.NullString - err := rows.Scan( + scanErr := rows.Scan( &subscription.ID, &subscription.UserDID, &subscription.CommunityDID, @@ -229,8 +240,8 @@ func (r *postgresCommunityRepo) ListSubscriptions(ctx context.Context, userDID s &recordURI, &recordCID, ) - if err != nil { - return nil, fmt.Errorf("failed to scan subscription: %w", err) + if scanErr != nil { + return nil, fmt.Errorf("failed to scan subscription: %w", scanErr) } subscription.RecordURI = recordURI.String @@ -259,14 +270,18 @@ func (r *postgresCommunityRepo) ListSubscribers(ctx context.Context, communityDI if err != nil { return nil, fmt.Errorf("failed to list subscribers: %w", err) } - defer rows.Close() + defer func() { + if closeErr := rows.Close(); closeErr != nil { + log.Printf("Failed to close rows: %v", closeErr) + } + }() result := []*communities.Subscription{} for rows.Next() { subscription := &communities.Subscription{} var recordURI, recordCID sql.NullString - err := rows.Scan( + scanErr := rows.Scan( &subscription.ID, &subscription.UserDID, &subscription.CommunityDID, @@ -274,8 +289,8 @@ func (r *postgresCommunityRepo) ListSubscribers(ctx context.Context, communityDI &recordURI, &recordCID, ) - if err != nil { - return nil, fmt.Errorf("failed to scan subscriber: %w", err) + if scanErr != nil { + return nil, fmt.Errorf("failed to scan subscriber: %w", scanErr) } subscription.RecordURI = recordURI.String