diff --git a/cmd/genjwks/main.go b/cmd/genjwks/main.go index 32d2a2c..d734487 100644 --- a/cmd/genjwks/main.go +++ b/cmd/genjwks/main.go @@ -16,7 +16,8 @@ import ( // The private key is stored in the config/env, public key is served at /oauth/jwks.json // // Usage: -// go run cmd/genjwks/main.go +// +// go run cmd/genjwks/main.go // // This will output a JSON private key that should be stored in OAUTH_PRIVATE_JWK func main() { @@ -35,13 +36,13 @@ func main() { } // Set key parameters - if err := jwkKey.Set(jwk.KeyIDKey, "oauth-client-key"); err != nil { + if err = jwkKey.Set(jwk.KeyIDKey, "oauth-client-key"); err != nil { log.Fatalf("Failed to set kid: %v", err) } - if err := jwkKey.Set(jwk.AlgorithmKey, "ES256"); err != nil { + if err = jwkKey.Set(jwk.AlgorithmKey, "ES256"); err != nil { log.Fatalf("Failed to set alg: %v", err) } - if err := jwkKey.Set(jwk.KeyUsageKey, "sig"); err != nil { + if err = jwkKey.Set(jwk.KeyUsageKey, "sig"); err != nil { log.Fatalf("Failed to set use: %v", err) } @@ -64,7 +65,7 @@ func main() { // Optionally write to a file (not committed) if len(os.Args) > 1 && os.Args[1] == "--save" { filename := "oauth-private-key.json" - if err := os.WriteFile(filename, jsonData, 0600); err != nil { + if err := os.WriteFile(filename, jsonData, 0o600); err != nil { log.Fatalf("Failed to write key file: %v", err) } fmt.Printf("\n💾 Private key saved to %s (remember to add to .gitignore!)\n", filename) diff --git a/internal/api/middleware/auth.go b/internal/api/middleware/auth.go index b1dcb94..7bb56a3 100644 --- a/internal/api/middleware/auth.go +++ b/internal/api/middleware/auth.go @@ -1,6 +1,7 @@ package middleware import ( + "Coves/internal/api/handlers/oauth" "context" "fmt" "log" @@ -8,7 +9,6 @@ import ( "os" "strings" - "Coves/internal/api/handlers/oauth" atprotoOAuth "Coves/internal/atproto/oauth" oauthCore "Coves/internal/core/oauth" ) diff --git a/internal/api/middleware/ratelimit.go b/internal/api/middleware/ratelimit.go index 8180bb0..4999233 100644 --- a/internal/api/middleware/ratelimit.go +++ b/internal/api/middleware/ratelimit.go @@ -9,15 +9,15 @@ import ( // RateLimiter implements a simple in-memory rate limiter // For production, consider using Redis or a distributed rate limiter type RateLimiter struct { - mu sync.Mutex clients map[string]*clientLimit - requests int // Max requests per window - window time.Duration // Time window + requests int + window time.Duration + mu sync.Mutex } type clientLimit struct { - count int resetTime time.Time + count int } // NewRateLimiter creates a new rate limiter diff --git a/internal/api/routes/community.go b/internal/api/routes/community.go index ad0f529..75d6fd4 100644 --- a/internal/api/routes/community.go +++ b/internal/api/routes/community.go @@ -1,10 +1,10 @@ package routes import ( - "github.com/go-chi/chi/v5" - "Coves/internal/api/handlers/community" "Coves/internal/core/communities" + + "github.com/go-chi/chi/v5" ) // RegisterCommunityRoutes registers community-related XRPC endpoints on the router diff --git a/internal/atproto/did/generator_test.go b/internal/atproto/did/generator_test.go index 1f569ea..6dcb8c2 100644 --- a/internal/atproto/did/generator_test.go +++ b/internal/atproto/did/generator_test.go @@ -8,9 +8,9 @@ import ( func TestGenerateCommunityDID(t *testing.T) { tests := []struct { name string - isDevEnv bool plcDirectoryURL string - want string // prefix we expect + want string + isDevEnv bool }{ { name: "generates did:plc in dev mode", @@ -30,7 +30,6 @@ func TestGenerateCommunityDID(t *testing.T) { t.Run(tt.name, func(t *testing.T) { g := NewGenerator(tt.isDevEnv, tt.plcDirectoryURL) did, err := g.GenerateCommunityDID() - if err != nil { t.Fatalf("GenerateCommunityDID() error = %v", err) } diff --git a/internal/atproto/identity/base_resolver.go b/internal/atproto/identity/base_resolver.go index 836eeca..0b5f75b 100644 --- a/internal/atproto/identity/base_resolver.go +++ b/internal/atproto/identity/base_resolver.go @@ -52,7 +52,6 @@ func (r *baseResolver) Resolve(ctx context.Context, identifier string) (*Identit // Resolve using Indigo's directory ident, err := r.directory.Lookup(ctx, *atID) - if err != nil { // Check if it's a "not found" error errStr := err.Error() diff --git a/internal/atproto/identity/factory.go b/internal/atproto/identity/factory.go index 8b109ff..74992f0 100644 --- a/internal/atproto/identity/factory.go +++ b/internal/atproto/identity/factory.go @@ -8,14 +8,9 @@ import ( // Config holds configuration for the identity resolver type Config struct { - // PLCURL is the URL of the PLC directory (default: https://plc.directory) - PLCURL string - - // CacheTTL is how long to cache resolved identities - CacheTTL time.Duration - - // HTTPClient for making HTTP requests (optional, will use default if nil) HTTPClient *http.Client + PLCURL string + CacheTTL time.Duration } // DefaultConfig returns a configuration with sensible defaults diff --git a/internal/atproto/lexicon/social/coves/richtext/facet_test.go b/internal/atproto/lexicon/social/coves/richtext/facet_test.go index 0108de4..93d5ed3 100644 --- a/internal/atproto/lexicon/social/coves/richtext/facet_test.go +++ b/internal/atproto/lexicon/social/coves/richtext/facet_test.go @@ -80,7 +80,7 @@ func TestFacetStructure(t *testing.T) { } return } - + // Basic validation if _, hasIndex := facet["index"]; !hasIndex && !tt.wantErr { t.Error("facet missing required 'index' field") @@ -148,11 +148,11 @@ func TestUTF8ByteCounting(t *testing.T) { break } } - + if idx == -1 { t.Fatalf("substring %q not found in text %q", tt.substring, tt.text) } - + // Calculate byte positions startByte := len([]byte(tt.text[:idx])) endByte := startByte + len([]byte(tt.substring)) @@ -170,10 +170,10 @@ func TestUTF8ByteCounting(t *testing.T) { // TestOverlappingFacets tests validation of overlapping facet ranges func TestOverlappingFacets(t *testing.T) { tests := []struct { - name string - facets []map[string]interface{} - expectError bool - description string + name string + description string + facets []map[string]interface{} + expectError bool }{ { name: "non-overlapping facets", @@ -191,8 +191,8 @@ func TestOverlappingFacets(t *testing.T) { }, }, }, - expectError: false, - description: "Facets with non-overlapping ranges should be valid", + expectError: false, + description: "Facets with non-overlapping ranges should be valid", }, { name: "exact same range", @@ -210,8 +210,8 @@ func TestOverlappingFacets(t *testing.T) { }, }, }, - expectError: false, - description: "Multiple facets on the same range are allowed (e.g., bold + italic)", + expectError: false, + description: "Multiple facets on the same range are allowed (e.g., bold + italic)", }, { name: "nested ranges", @@ -229,8 +229,8 @@ func TestOverlappingFacets(t *testing.T) { }, }, }, - expectError: false, - description: "Nested facet ranges are allowed", + expectError: false, + description: "Nested facet ranges are allowed", }, { name: "partial overlap", @@ -248,8 +248,8 @@ func TestOverlappingFacets(t *testing.T) { }, }, }, - expectError: false, - description: "Partially overlapping facets are allowed", + expectError: false, + description: "Partially overlapping facets are allowed", }, } @@ -268,9 +268,9 @@ func TestOverlappingFacets(t *testing.T) { // TestFacetFeatureTypes tests all supported facet feature types func TestFacetFeatureTypes(t *testing.T) { featureTypes := []struct { + feature map[string]interface{} name string typeName string - feature map[string]interface{} }{ { name: "mention", @@ -348,4 +348,4 @@ func TestFacetFeatureTypes(t *testing.T) { } }) } -} \ No newline at end of file +} diff --git a/internal/atproto/oauth/client.go b/internal/atproto/oauth/client.go index 03910cc..697e28b 100644 --- a/internal/atproto/oauth/client.go +++ b/internal/atproto/oauth/client.go @@ -15,10 +15,10 @@ import ( // Client handles atProto OAuth flows (PAR, PKCE, DPoP) type Client struct { - clientID string clientJWK jwk.Key - redirectURI string httpClient *http.Client + clientID string + redirectURI string } // NewClient creates a new OAuth client @@ -114,10 +114,10 @@ func (c *Client) FetchAuthServerMetadata(ctx context.Context, issuer string) (*A // PARResponse represents the response from a Pushed Authorization Request type PARResponse struct { RequestURI string `json:"request_uri"` - ExpiresIn int `json:"expires_in"` - State string // Generated by client - PKCEVerifier string // Generated by client - DpopAuthserverNonce string // From response header (if provided) + State string + PKCEVerifier string + DpopAuthserverNonce string + ExpiresIn int `json:"expires_in"` } // SendPARRequest sends a Pushed Authorization Request (PAR) - RFC 9126 @@ -239,12 +239,12 @@ func (c *Client) SendPARRequest(ctx context.Context, authMeta *AuthServerMetadat // TokenResponse represents an OAuth token response type TokenResponse struct { AccessToken string `json:"access_token"` - TokenType string `json:"token_type"` // Should be "DPoP" - ExpiresIn int `json:"expires_in"` + TokenType string `json:"token_type"` RefreshToken string `json:"refresh_token"` Scope string `json:"scope"` - Sub string `json:"sub"` // DID of the user - DpopAuthserverNonce string // From response header + Sub string `json:"sub"` + DpopAuthserverNonce string + ExpiresIn int `json:"expires_in"` } // InitialTokenRequest exchanges authorization code for tokens (DPoP-bound) diff --git a/internal/core/communities/community.go b/internal/core/communities/community.go index c4bad9b..2fe4936 100644 --- a/internal/core/communities/community.go +++ b/internal/core/communities/community.go @@ -7,93 +7,71 @@ import ( // Community represents a Coves community indexed from the firehose // Communities are federated, instance-scoped forums built on atProto type Community struct { - ID int `json:"id" db:"id"` - DID string `json:"did" db:"did"` // Permanent community identifier (did:plc:xxx) - Handle string `json:"handle" db:"handle"` // Scoped handle (!gaming@coves.social) - Name string `json:"name" db:"name"` // Short name (local part of handle) - DisplayName string `json:"displayName" db:"display_name"` // Display name for UI - Description string `json:"description" db:"description"` // Community description - DescriptionFacets []byte `json:"descriptionFacets,omitempty" db:"description_facets"` // Rich text annotations (JSONB) - - // Media - AvatarCID string `json:"avatarCid,omitempty" db:"avatar_cid"` // CID of avatar image - BannerCID string `json:"bannerCid,omitempty" db:"banner_cid"` // CID of banner image - - // Ownership - OwnerDID string `json:"ownerDid" db:"owner_did"` // V2: same as DID (community owns itself) - CreatedByDID string `json:"createdByDid" db:"created_by_did"` // User who created the community - HostedByDID string `json:"hostedByDid" db:"hosted_by_did"` // Instance hosting this community - - // V2: PDS Account Credentials (NEVER expose in public API responses!) - PDSEmail string `json:"-" db:"pds_email"` // System email for PDS account - PDSPasswordHash string `json:"-" db:"pds_password_hash"` // bcrypt hash for re-authentication - PDSAccessToken string `json:"-" db:"pds_access_token"` // JWT for API calls (expires) - PDSRefreshToken string `json:"-" db:"pds_refresh_token"` // For refreshing sessions - PDSURL string `json:"-" db:"pds_url"` // PDS hosting this community's repo - - // Visibility & Federation - Visibility string `json:"visibility" db:"visibility"` // public, unlisted, private - AllowExternalDiscovery bool `json:"allowExternalDiscovery" db:"allow_external_discovery"` // Can other instances index? - - // Moderation - ModerationType string `json:"moderationType,omitempty" db:"moderation_type"` // moderator, sortition - ContentWarnings []string `json:"contentWarnings,omitempty" db:"content_warnings"` // NSFW, violence, spoilers - - // Statistics (cached counts) - MemberCount int `json:"memberCount" db:"member_count"` - SubscriberCount int `json:"subscriberCount" db:"subscriber_count"` - PostCount int `json:"postCount" db:"post_count"` - - // Federation metadata (future: Lemmy interop) - FederatedFrom string `json:"federatedFrom,omitempty" db:"federated_from"` // lemmy, coves - FederatedID string `json:"federatedId,omitempty" db:"federated_id"` // Original ID on source platform - - // Timestamps - CreatedAt time.Time `json:"createdAt" db:"created_at"` - UpdatedAt time.Time `json:"updatedAt" db:"updated_at"` - - // AT-Proto metadata - RecordURI string `json:"recordUri,omitempty" db:"record_uri"` // AT-URI of community profile record - RecordCID string `json:"recordCid,omitempty" db:"record_cid"` // CID of community profile record + CreatedAt time.Time `json:"createdAt" db:"created_at"` + UpdatedAt time.Time `json:"updatedAt" db:"updated_at"` + PDSAccessToken string `json:"-" db:"pds_access_token"` + FederatedID string `json:"federatedId,omitempty" db:"federated_id"` + DisplayName string `json:"displayName" db:"display_name"` + Description string `json:"description" db:"description"` + PDSURL string `json:"-" db:"pds_url"` + AvatarCID string `json:"avatarCid,omitempty" db:"avatar_cid"` + BannerCID string `json:"bannerCid,omitempty" db:"banner_cid"` + OwnerDID string `json:"ownerDid" db:"owner_did"` + CreatedByDID string `json:"createdByDid" db:"created_by_did"` + HostedByDID string `json:"hostedByDid" db:"hosted_by_did"` + PDSEmail string `json:"-" db:"pds_email"` + PDSPasswordHash string `json:"-" db:"pds_password_hash"` + Name string `json:"name" db:"name"` + RecordCID string `json:"recordCid,omitempty" db:"record_cid"` + RecordURI string `json:"recordUri,omitempty" db:"record_uri"` + Visibility string `json:"visibility" db:"visibility"` + DID string `json:"did" db:"did"` + ModerationType string `json:"moderationType,omitempty" db:"moderation_type"` + Handle string `json:"handle" db:"handle"` + PDSRefreshToken string `json:"-" db:"pds_refresh_token"` + FederatedFrom string `json:"federatedFrom,omitempty" db:"federated_from"` + ContentWarnings []string `json:"contentWarnings,omitempty" db:"content_warnings"` + DescriptionFacets []byte `json:"descriptionFacets,omitempty" db:"description_facets"` + PostCount int `json:"postCount" db:"post_count"` + SubscriberCount int `json:"subscriberCount" db:"subscriber_count"` + MemberCount int `json:"memberCount" db:"member_count"` + ID int `json:"id" db:"id"` + AllowExternalDiscovery bool `json:"allowExternalDiscovery" db:"allow_external_discovery"` } // Subscription represents a lightweight feed follow (user subscribes to see posts) type Subscription struct { - ID int `json:"id" db:"id"` + SubscribedAt time.Time `json:"subscribedAt" db:"subscribed_at"` UserDID string `json:"userDid" db:"user_did"` CommunityDID string `json:"communityDid" db:"community_did"` - SubscribedAt time.Time `json:"subscribedAt" db:"subscribed_at"` - - // AT-Proto metadata (subscription is a record in user's repo) - RecordURI string `json:"recordUri,omitempty" db:"record_uri"` - RecordCID string `json:"recordCid,omitempty" db:"record_cid"` + RecordURI string `json:"recordUri,omitempty" db:"record_uri"` + RecordCID string `json:"recordCid,omitempty" db:"record_cid"` + ID int `json:"id" db:"id"` } // Membership represents active participation with reputation tracking type Membership struct { - ID int `json:"id" db:"id"` - UserDID string `json:"userDid" db:"user_did"` - CommunityDID string `json:"communityDid" db:"community_did"` - ReputationScore int `json:"reputationScore" db:"reputation_score"` // Gained through participation - ContributionCount int `json:"contributionCount" db:"contribution_count"` // Posts + comments + actions JoinedAt time.Time `json:"joinedAt" db:"joined_at"` LastActiveAt time.Time `json:"lastActiveAt" db:"last_active_at"` - - // Moderation status - IsBanned bool `json:"isBanned" db:"is_banned"` - IsModerator bool `json:"isModerator" db:"is_moderator"` + UserDID string `json:"userDid" db:"user_did"` + CommunityDID string `json:"communityDid" db:"community_did"` + ID int `json:"id" db:"id"` + ReputationScore int `json:"reputationScore" db:"reputation_score"` + ContributionCount int `json:"contributionCount" db:"contribution_count"` + IsBanned bool `json:"isBanned" db:"is_banned"` + IsModerator bool `json:"isModerator" db:"is_moderator"` } // ModerationAction represents a moderation action taken against a community type ModerationAction struct { - ID int `json:"id" db:"id"` - CommunityDID string `json:"communityDid" db:"community_did"` - Action string `json:"action" db:"action"` // delist, quarantine, remove - Reason string `json:"reason,omitempty" db:"reason"` - InstanceDID string `json:"instanceDid" db:"instance_did"` // Which instance took this action - Broadcast bool `json:"broadcast" db:"broadcast"` // Share signal with network? - CreatedAt time.Time `json:"createdAt" db:"created_at"` - ExpiresAt *time.Time `json:"expiresAt,omitempty" db:"expires_at"` // Optional: temporary moderation + CreatedAt time.Time `json:"createdAt" db:"created_at"` + ExpiresAt *time.Time `json:"expiresAt,omitempty" db:"expires_at"` + CommunityDID string `json:"communityDid" db:"community_did"` + Action string `json:"action" db:"action"` + Reason string `json:"reason,omitempty" db:"reason"` + InstanceDID string `json:"instanceDid" db:"instance_did"` + ID int `json:"id" db:"id"` + Broadcast bool `json:"broadcast" db:"broadcast"` } // CreateCommunityRequest represents input for creating a new community @@ -101,21 +79,21 @@ type CreateCommunityRequest struct { Name string `json:"name"` DisplayName string `json:"displayName,omitempty"` Description string `json:"description"` - AvatarBlob []byte `json:"avatarBlob,omitempty"` // Raw image data - BannerBlob []byte `json:"bannerBlob,omitempty"` // Raw image data + Language string `json:"language,omitempty"` + Visibility string `json:"visibility"` + CreatedByDID string `json:"createdByDid"` + HostedByDID string `json:"hostedByDid"` + AvatarBlob []byte `json:"avatarBlob,omitempty"` + BannerBlob []byte `json:"bannerBlob,omitempty"` Rules []string `json:"rules,omitempty"` Categories []string `json:"categories,omitempty"` - Language string `json:"language,omitempty"` - Visibility string `json:"visibility"` // public, unlisted, private AllowExternalDiscovery bool `json:"allowExternalDiscovery"` - CreatedByDID string `json:"createdByDid"` // User creating the community - HostedByDID string `json:"hostedByDid"` // Instance hosting the community } // UpdateCommunityRequest represents input for updating community metadata type UpdateCommunityRequest struct { CommunityDID string `json:"communityDid"` - UpdatedByDID string `json:"updatedByDid"` // User making the update (for authorization) + UpdatedByDID string `json:"updatedByDid"` // User making the update (for authorization) DisplayName *string `json:"displayName,omitempty"` Description *string `json:"description,omitempty"` AvatarBlob []byte `json:"avatarBlob,omitempty"` @@ -128,18 +106,18 @@ type UpdateCommunityRequest struct { // ListCommunitiesRequest represents query parameters for listing communities type ListCommunitiesRequest struct { + Visibility string `json:"visibility,omitempty"` + HostedBy string `json:"hostedBy,omitempty"` + SortBy string `json:"sortBy,omitempty"` + SortOrder string `json:"sortOrder,omitempty"` Limit int `json:"limit"` Offset int `json:"offset"` - Visibility string `json:"visibility,omitempty"` // Filter by visibility - HostedBy string `json:"hostedBy,omitempty"` // Filter by hosting instance - SortBy string `json:"sortBy,omitempty"` // created_at, member_count, post_count - SortOrder string `json:"sortOrder,omitempty"` // asc, desc } // SearchCommunitiesRequest represents query parameters for searching communities type SearchCommunitiesRequest struct { - Query string `json:"query"` // Search term + Query string `json:"query"` + Visibility string `json:"visibility,omitempty"` Limit int `json:"limit"` Offset int `json:"offset"` - Visibility string `json:"visibility,omitempty"` // Filter by visibility } diff --git a/internal/core/communities/pds_provisioning.go b/internal/core/communities/pds_provisioning.go index 5c59096..82a31ef 100644 --- a/internal/core/communities/pds_provisioning.go +++ b/internal/core/communities/pds_provisioning.go @@ -1,13 +1,13 @@ package communities import ( + "Coves/internal/core/users" "context" "crypto/rand" "encoding/base64" "fmt" "strings" - "Coves/internal/core/users" "golang.org/x/crypto/bcrypt" ) @@ -30,7 +30,7 @@ type PDSAccountProvisioner struct { } // NewPDSAccountProvisioner creates a new provisioner -func NewPDSAccountProvisioner(userService users.UserService, instanceDomain string, pdsURL string) *PDSAccountProvisioner { +func NewPDSAccountProvisioner(userService users.UserService, instanceDomain, pdsURL string) *PDSAccountProvisioner { return &PDSAccountProvisioner{ userService: userService, instanceDomain: instanceDomain, @@ -107,13 +107,13 @@ func (p *PDSAccountProvisioner) ProvisionCommunityAccount( // 6. Return account credentials return &CommunityPDSAccount{ - DID: resp.DID, // The community's DID - it owns its own repository! - Handle: resp.Handle, // e.g., gaming.coves.social - Email: email, // community-gaming@system.coves.social + DID: resp.DID, // The community's DID - it owns its own repository! + Handle: resp.Handle, // e.g., gaming.coves.social + Email: email, // community-gaming@system.coves.social PasswordHash: string(passwordHash), // bcrypt hash for re-authentication - AccessToken: resp.AccessJwt, // JWT for making API calls as the community - RefreshToken: resp.RefreshJwt, // For refreshing sessions when access token expires - PDSURL: resp.PDSURL, // PDS hosting this community's repository + AccessToken: resp.AccessJwt, // JWT for making API calls as the community + RefreshToken: resp.RefreshJwt, // For refreshing sessions when access token expires + PDSURL: resp.PDSURL, // PDS hosting this community's repository }, nil } diff --git a/internal/core/errors/errors.go b/internal/core/errors/errors.go index 40bec72..0b27bdf 100644 --- a/internal/core/errors/errors.go +++ b/internal/core/errors/errors.go @@ -36,8 +36,8 @@ func (e ConflictError) Error() string { } type NotFoundError struct { - Resource string ID interface{} + Resource string } func (e NotFoundError) Error() string { @@ -64,4 +64,4 @@ func NewNotFoundError(resource string, id interface{}) error { Resource: resource, ID: id, } -} \ No newline at end of file +} diff --git a/internal/core/oauth/repository.go b/internal/core/oauth/repository.go index 944bc18..1400c87 100644 --- a/internal/core/oauth/repository.go +++ b/internal/core/oauth/repository.go @@ -38,7 +38,6 @@ func (s *PostgresSessionStore) SaveRequest(req *OAuthRequest) error { req.AuthServerIss, req.ReturnURL, ) - if err != nil { return fmt.Errorf("failed to save OAuth request: %w", err) } @@ -163,7 +162,6 @@ func (s *PostgresSessionStore) SaveSession(session *OAuthSession) error { session.AuthServerIss, session.ExpiresAt, ) - if err != nil { return fmt.Errorf("failed to save OAuth session: %w", err) } @@ -240,7 +238,6 @@ func (s *PostgresSessionStore) UpdateSession(session *OAuthSession) error { session.AuthServerIss, session.ExpiresAt, ) - if err != nil { return fmt.Errorf("failed to update OAuth session: %w", err) } diff --git a/internal/core/oauth/session.go b/internal/core/oauth/session.go index 6be4773..be2b7a1 100644 --- a/internal/core/oauth/session.go +++ b/internal/core/oauth/session.go @@ -7,33 +7,33 @@ import ( // OAuthRequest represents a temporary OAuth authorization flow state // Stored during the redirect to auth server, deleted after callback type OAuthRequest struct { + CreatedAt time.Time `db:"created_at"` State string `db:"state"` DID string `db:"did"` Handle string `db:"handle"` PDSURL string `db:"pds_url"` PKCEVerifier string `db:"pkce_verifier"` - DPoPPrivateJWK string `db:"dpop_private_jwk"` // JSON-encoded JWK + DPoPPrivateJWK string `db:"dpop_private_jwk"` DPoPAuthServerNonce string `db:"dpop_authserver_nonce"` AuthServerIss string `db:"auth_server_iss"` ReturnURL string `db:"return_url"` - CreatedAt time.Time `db:"created_at"` } // OAuthSession represents a long-lived authenticated user session // Stored after successful OAuth login, used for all authenticated requests type OAuthSession struct { + ExpiresAt time.Time `db:"expires_at"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` DID string `db:"did"` Handle string `db:"handle"` PDSURL string `db:"pds_url"` AccessToken string `db:"access_token"` RefreshToken string `db:"refresh_token"` - DPoPPrivateJWK string `db:"dpop_private_jwk"` // JSON-encoded JWK + DPoPPrivateJWK string `db:"dpop_private_jwk"` DPoPAuthServerNonce string `db:"dpop_authserver_nonce"` DPoPPDSNonce string `db:"dpop_pds_nonce"` AuthServerIss string `db:"auth_server_iss"` - ExpiresAt time.Time `db:"expires_at"` - CreatedAt time.Time `db:"created_at"` - UpdatedAt time.Time `db:"updated_at"` } // SessionStore defines the interface for OAuth session storage diff --git a/internal/core/users/errors.go b/internal/core/users/errors.go index db0fcdc..c176d6a 100644 --- a/internal/core/users/errors.go +++ b/internal/core/users/errors.go @@ -48,8 +48,8 @@ func (e *WeakPasswordError) Error() string { // PDSError wraps errors from the PDS that we couldn't map to domain errors type PDSError struct { - StatusCode int Message string + StatusCode int } func (e *PDSError) Error() string { diff --git a/internal/core/users/interfaces.go b/internal/core/users/interfaces.go index 44d1b23..b08ea4e 100644 --- a/internal/core/users/interfaces.go +++ b/internal/core/users/interfaces.go @@ -18,4 +18,4 @@ type UserService interface { UpdateHandle(ctx context.Context, did, newHandle string) (*User, error) ResolveHandleToDID(ctx context.Context, handle string) (string, error) RegisterAccount(ctx context.Context, req RegisterAccountRequest) (*RegisterAccountResponse, error) -} \ No newline at end of file +} diff --git a/internal/core/users/user.go b/internal/core/users/user.go index 009707d..5bb4a2f 100644 --- a/internal/core/users/user.go +++ b/internal/core/users/user.go @@ -8,11 +8,11 @@ import ( // This is NOT the user's repository - that lives in the PDS // This table only tracks metadata for efficient AppView queries type User struct { - DID string `json:"did" db:"did"` // atProto DID (e.g., did:plc:xyz123) - Handle string `json:"handle" db:"handle"` // Human-readable handle (e.g., alice.coves.dev) - PDSURL string `json:"pdsUrl" db:"pds_url"` // User's PDS host URL (supports federation) CreatedAt time.Time `json:"createdAt" db:"created_at"` UpdatedAt time.Time `json:"updatedAt" db:"updated_at"` + DID string `json:"did" db:"did"` + Handle string `json:"handle" db:"handle"` + PDSURL string `json:"pdsUrl" db:"pds_url"` } // CreateUserRequest represents the input for creating a new user @@ -32,9 +32,9 @@ type RegisterAccountRequest struct { // RegisterAccountResponse represents the response from PDS account creation type RegisterAccountResponse struct { - DID string `json:"did"` - Handle string `json:"handle"` - AccessJwt string `json:"accessJwt"` - RefreshJwt string `json:"refreshJwt"` - PDSURL string `json:"pdsUrl"` + DID string `json:"did"` + Handle string `json:"handle"` + AccessJwt string `json:"accessJwt"` + RefreshJwt string `json:"refreshJwt"` + PDSURL string `json:"pdsUrl"` } diff --git a/internal/db/postgres/user_repo.go b/internal/db/postgres/user_repo.go index 9806998..7ecb4a3 100644 --- a/internal/db/postgres/user_repo.go +++ b/internal/db/postgres/user_repo.go @@ -1,12 +1,11 @@ package postgres import ( + "Coves/internal/core/users" "context" "database/sql" "fmt" "strings" - - "Coves/internal/core/users" ) type postgresUserRepo struct { @@ -27,7 +26,6 @@ func (r *postgresUserRepo) Create(ctx context.Context, user *users.User) (*users err := r.db.QueryRowContext(ctx, query, user.DID, user.Handle, user.PDSURL). Scan(&user.DID, &user.Handle, &user.PDSURL, &user.CreatedAt, &user.UpdatedAt) - if err != nil { // Check for unique constraint violations if strings.Contains(err.Error(), "duplicate key") { diff --git a/internal/validation/lexicon.go b/internal/validation/lexicon.go index 9432c4e..10db80d 100644 --- a/internal/validation/lexicon.go +++ b/internal/validation/lexicon.go @@ -16,7 +16,7 @@ type LexiconValidator struct { // NewLexiconValidator creates a new validator with the specified schema directory func NewLexiconValidator(schemaPath string, strict bool) (*LexiconValidator, error) { catalog := lexicon.NewBaseCatalog() - + if err := catalog.LoadDirectory(schemaPath); err != nil { return nil, fmt.Errorf("failed to load lexicon schemas: %w", err) } @@ -38,7 +38,7 @@ func NewLexiconValidator(schemaPath string, strict bool) (*LexiconValidator, err func (v *LexiconValidator) ValidateRecord(recordData interface{}, recordType string) error { // Convert to map if needed var data map[string]interface{} - + switch rd := recordData.(type) { case map[string]interface{}: data = rd @@ -107,4 +107,4 @@ func (v *LexiconValidator) ResolveReference(ref string) (interface{}, error) { // GetCatalog returns the underlying lexicon catalog for advanced usage func (v *LexiconValidator) GetCatalog() *lexicon.BaseCatalog { return v.catalog -} \ No newline at end of file +} diff --git a/internal/validation/lexicon_test.go b/internal/validation/lexicon_test.go index 23f87e1..475b028 100644 --- a/internal/validation/lexicon_test.go +++ b/internal/validation/lexicon_test.go @@ -132,4 +132,4 @@ func TestStrictValidation(t *testing.T) { if err := validator.ValidateActorProfile(profile); err == nil { t.Error("Expected strict validation to fail on datetime without timezone") } -} \ No newline at end of file +}