A community based topic aggregation platform built on atproto

refactor(server): update initialization for new auth system

Server and infrastructure updates:
- Initialize auth middleware with JWT validation
- Remove OAuth route registration
- Update imports to use new auth package
- Clean up unused OAuth configuration
- Update PDS provisioning comments for clarity
- Fix repository query parameter ordering

These changes complete the migration from OAuth to JWT-based auth
throughout the application initialization and routing layers.

Changed files
+41 -68
cmd
server
internal
api
routes
core
communities
db
+20 -46
cmd/server/main.go
···
package main
import (
-
"Coves/internal/api/handlers/oauth"
"Coves/internal/api/middleware"
"Coves/internal/api/routes"
"Coves/internal/atproto/identity"
"Coves/internal/atproto/jetstream"
"Coves/internal/core/communities"
···
chiMiddleware "github.com/go-chi/chi/v5/middleware"
_ "github.com/lib/pq"
"github.com/pressly/goose/v3"
-
-
oauthCore "Coves/internal/core/oauth"
postgresRepo "Coves/internal/db/postgres"
)
···
identityResolver := identity.NewResolver(db, identityConfig)
-
// Initialize OAuth session store
-
sessionStore := oauthCore.NewPostgresSessionStore(db)
-
log.Println("OAuth session store initialized")
// Initialize repositories and services
userRepo := postgresRepo.NewUserRepository(db)
···
// they appear in the firehose. A dedicated consumer can be added later if needed.
log.Println("Community event consumer initialized (processes events from firehose)")
-
// Start OAuth cleanup background job
go func() {
ticker := time.NewTicker(1 * time.Hour)
defer ticker.Stop()
for range ticker.C {
-
if pgStore, ok := sessionStore.(*oauthCore.PostgresSessionStore); ok {
-
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")
-
}
}
}()
-
log.Println("Started OAuth cleanup background job (runs hourly)")
-
-
// Initialize OAuth cookie store (singleton)
-
cookieSecret, err := oauth.GetEnvBase64OrPlain("OAUTH_COOKIE_SECRET")
-
if err != nil {
-
log.Fatalf("Failed to load OAUTH_COOKIE_SECRET: %v", err)
-
}
-
if cookieSecret == "" {
-
log.Fatal("OAUTH_COOKIE_SECRET not configured")
-
}
-
-
if err := oauth.InitCookieStore(cookieSecret); err != nil {
-
log.Fatalf("Failed to initialize cookie store: %v", err)
-
}
-
-
// Initialize OAuth handlers
-
loginHandler := oauth.NewLoginHandler(identityResolver, sessionStore)
-
callbackHandler := oauth.NewCallbackHandler(sessionStore)
-
logoutHandler := oauth.NewLogoutHandler(sessionStore)
-
-
// OAuth routes (public endpoints)
-
r.Post("/oauth/login", loginHandler.HandleLogin)
-
r.Get("/oauth/callback", callbackHandler.HandleCallback)
-
r.Post("/oauth/logout", logoutHandler.HandleLogout)
-
r.Get("/oauth/client-metadata.json", oauth.HandleClientMetadata)
-
r.Get("/oauth/jwks.json", oauth.HandleJWKS)
-
-
log.Println("OAuth endpoints registered")
// Register XRPC routes
routes.RegisterUserRoutes(r, userService)
-
routes.RegisterCommunityRoutes(r, communityService)
-
log.Println("Community XRPC endpoints registered")
r.Get("/health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
···
package main
import (
"Coves/internal/api/middleware"
"Coves/internal/api/routes"
+
"Coves/internal/atproto/auth"
"Coves/internal/atproto/identity"
"Coves/internal/atproto/jetstream"
"Coves/internal/core/communities"
···
chiMiddleware "github.com/go-chi/chi/v5/middleware"
_ "github.com/lib/pq"
"github.com/pressly/goose/v3"
postgresRepo "Coves/internal/db/postgres"
)
···
identityResolver := identity.NewResolver(db, identityConfig)
+
// Initialize atProto auth middleware for JWT validation
+
// Phase 1: Set skipVerify=true to test JWT parsing only
+
// Phase 2: Set skipVerify=false to enable full signature verification
+
skipVerify := os.Getenv("AUTH_SKIP_VERIFY") == "true"
+
if skipVerify {
+
log.Println("⚠️ WARNING: JWT signature verification is DISABLED (Phase 1 testing)")
+
log.Println(" Set AUTH_SKIP_VERIFY=false for production")
+
}
+
+
jwksCacheTTL := 1 * time.Hour // Cache public keys for 1 hour
+
jwksFetcher := auth.NewCachedJWKSFetcher(jwksCacheTTL)
+
authMiddleware := middleware.NewAtProtoAuthMiddleware(jwksFetcher, skipVerify)
+
log.Println("✅ atProto auth middleware initialized")
// Initialize repositories and services
userRepo := postgresRepo.NewUserRepository(db)
···
// they appear in the firehose. A dedicated consumer can be added later if needed.
log.Println("Community event consumer initialized (processes events from firehose)")
+
// Start JWKS cache cleanup background job
go func() {
ticker := time.NewTicker(1 * time.Hour)
defer ticker.Stop()
for range ticker.C {
+
jwksFetcher.CleanupExpiredCache()
+
log.Println("JWKS cache cleanup completed")
}
}()
+
log.Println("Started JWKS cache cleanup background job (runs hourly)")
// Register XRPC routes
routes.RegisterUserRoutes(r, userService)
+
routes.RegisterCommunityRoutes(r, communityService, authMiddleware)
+
log.Println("Community XRPC endpoints registered with OAuth authentication")
r.Get("/health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
+9 -8
internal/api/routes/community.go
···
import (
"Coves/internal/api/handlers/community"
"Coves/internal/core/communities"
"github.com/go-chi/chi/v5"
···
// RegisterCommunityRoutes registers community-related XRPC endpoints on the router
// Implements social.coves.community.* lexicon endpoints
-
func RegisterCommunityRoutes(r chi.Router, service communities.Service) {
// Initialize handlers
createHandler := community.NewCreateHandler(service)
getHandler := community.NewGetHandler(service)
···
searchHandler := community.NewSearchHandler(service)
subscribeHandler := community.NewSubscribeHandler(service)
-
// Query endpoints (GET)
// social.coves.community.get - get a single community by identifier
r.Get("/xrpc/social.coves.community.get", getHandler.HandleGet)
···
// social.coves.community.search - search communities
r.Get("/xrpc/social.coves.community.search", searchHandler.HandleSearch)
-
// Procedure endpoints (POST) - write-forward operations
// social.coves.community.create - create a new community
-
r.Post("/xrpc/social.coves.community.create", createHandler.HandleCreate)
// social.coves.community.update - update an existing community
-
r.Post("/xrpc/social.coves.community.update", updateHandler.HandleUpdate)
// social.coves.community.subscribe - subscribe to a community
-
r.Post("/xrpc/social.coves.community.subscribe", subscribeHandler.HandleSubscribe)
// social.coves.community.unsubscribe - unsubscribe from a community
-
r.Post("/xrpc/social.coves.community.unsubscribe", subscribeHandler.HandleUnsubscribe)
// TODO: Add delete handler when implemented
-
// r.Post("/xrpc/social.coves.community.delete", deleteHandler.HandleDelete)
}
···
import (
"Coves/internal/api/handlers/community"
+
"Coves/internal/api/middleware"
"Coves/internal/core/communities"
"github.com/go-chi/chi/v5"
···
// RegisterCommunityRoutes registers community-related XRPC endpoints on the router
// Implements social.coves.community.* lexicon endpoints
+
func RegisterCommunityRoutes(r chi.Router, service communities.Service, authMiddleware *middleware.AtProtoAuthMiddleware) {
// Initialize handlers
createHandler := community.NewCreateHandler(service)
getHandler := community.NewGetHandler(service)
···
searchHandler := community.NewSearchHandler(service)
subscribeHandler := community.NewSubscribeHandler(service)
+
// Query endpoints (GET) - public access
// social.coves.community.get - get a single community by identifier
r.Get("/xrpc/social.coves.community.get", getHandler.HandleGet)
···
// social.coves.community.search - search communities
r.Get("/xrpc/social.coves.community.search", searchHandler.HandleSearch)
+
// Procedure endpoints (POST) - require authentication
// social.coves.community.create - create a new community
+
r.With(authMiddleware.RequireAuth).Post("/xrpc/social.coves.community.create", createHandler.HandleCreate)
// social.coves.community.update - update an existing community
+
r.With(authMiddleware.RequireAuth).Post("/xrpc/social.coves.community.update", updateHandler.HandleUpdate)
// social.coves.community.subscribe - subscribe to a community
+
r.With(authMiddleware.RequireAuth).Post("/xrpc/social.coves.community.subscribe", subscribeHandler.HandleSubscribe)
// social.coves.community.unsubscribe - unsubscribe from a community
+
r.With(authMiddleware.RequireAuth).Post("/xrpc/social.coves.community.unsubscribe", subscribeHandler.HandleUnsubscribe)
// TODO: Add delete handler when implemented
+
// r.With(authMiddleware.RequireAuth).Post("/xrpc/social.coves.community.delete", deleteHandler.HandleDelete)
}
+9 -11
internal/core/communities/pds_provisioning.go
···
// CommunityPDSAccount represents PDS account credentials for a community
type CommunityPDSAccount struct {
-
DID string // Community's DID (owns the repository)
-
Handle string // Community's handle (e.g., gaming.communities.coves.social)
-
Email string // System email for PDS account
-
Password string // Cleartext password (MUST be encrypted before database storage)
-
AccessToken string // JWT for making API calls as the community
-
RefreshToken string // For refreshing sessions
-
PDSURL string // PDS hosting this community
-
RotationKeyPEM string // PEM-encoded rotation key (for portability)
-
SigningKeyPEM string // PEM-encoded signing key (for atproto operations)
}
// PDSAccountProvisioner creates PDS accounts for communities with PDS-managed DIDs
···
return password, nil
}
-
// FetchPDSDID queries the PDS to get its DID via com.atproto.server.describeServer
// This is the proper way to get the PDS DID rather than hardcoding it
// Works in both development (did:web:localhost) and production (did:web:pds.example.com)
···
return resp.Did, nil
}
-
···
// CommunityPDSAccount represents PDS account credentials for a community
type CommunityPDSAccount struct {
+
DID string // Community's DID (owns the repository)
+
Handle string // Community's handle (e.g., gaming.communities.coves.social)
+
Email string // System email for PDS account
+
Password string // Cleartext password (MUST be encrypted before database storage)
+
AccessToken string // JWT for making API calls as the community
+
RefreshToken string // For refreshing sessions
+
PDSURL string // PDS hosting this community
+
RotationKeyPEM string // PEM-encoded rotation key (for portability)
+
SigningKeyPEM string // PEM-encoded signing key (for atproto operations)
}
// PDSAccountProvisioner creates PDS accounts for communities with PDS-managed DIDs
···
return password, nil
}
// FetchPDSDID queries the PDS to get its DID via com.atproto.server.describeServer
// This is the proper way to get the PDS DID rather than hardcoding it
// Works in both development (did:web:localhost) and production (did:web:pds.example.com)
···
return resp.Did, nil
}
+3 -3
internal/db/postgres/community_repo.go
···
community.HostedByDID,
// V2.0: PDS credentials for community account (encrypted at rest)
nullString(community.PDSEmail),
-
nullString(community.PDSPassword), // Encrypted by pgp_sym_encrypt
-
nullString(community.PDSAccessToken), // Encrypted by pgp_sym_encrypt
-
nullString(community.PDSRefreshToken), // Encrypted by pgp_sym_encrypt
nullString(community.PDSURL),
// V2.0: No key columns - PDS manages all keys
community.Visibility,
···
community.HostedByDID,
// V2.0: PDS credentials for community account (encrypted at rest)
nullString(community.PDSEmail),
+
nullString(community.PDSPassword), // Encrypted by pgp_sym_encrypt
+
nullString(community.PDSAccessToken), // Encrypted by pgp_sym_encrypt
+
nullString(community.PDSRefreshToken), // Encrypted by pgp_sym_encrypt
nullString(community.PDSURL),
// V2.0: No key columns - PDS manages all keys
community.Visibility,