refactor: narrow auth service to smaller units

Easier for testing, and omits need for full dep injection for pusher service, where we passed in nil for deps dangerously.
This commit is contained in:
Arjun Patel
2026-04-27 16:14:37 -07:00
parent 48d6d5cb07
commit 0f73d685c2
4 changed files with 60 additions and 26 deletions
+3 -3
View File
@@ -36,8 +36,8 @@ func main() {
pusherRedis := internal.ConnectAndTestRedis(db.RedisDBPusher)
// Services
authSvc := auth.NewAuthService(authRedis, nil, nil) // nil aeroSvc / fbAuth — pusher only calls GetSession
networkReader := network.NewReader(db.Pool()) // pusher only checks membership
sessionReader := auth.NewSessionReader(authRedis) // pusher only validates sessions
networkReader := network.NewReader(db.Pool()) // pusher only checks membership
// Pod identity (use hostname in k8s, which is the pod name)
podID, err := os.Hostname()
@@ -54,7 +54,7 @@ func main() {
authorizer := pusher.NewAuthorizer(networkReader)
hub := pusher.NewHub(bridge, authorizer)
bridge.SetHub(hub)
server := pusher.NewServer(ctx, hub, bridge, authSvc)
server := pusher.NewServer(ctx, hub, bridge, sessionReader)
// Start hub event loop
go hub.Run(ctx)
+45
View File
@@ -0,0 +1,45 @@
package auth
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/redis/go-redis/v9"
)
type SessionReader interface {
// GetSession returns ErrSessionNotFound if no valid session.
GetSession(ctx context.Context, sessionToken string) (*Session, error)
}
type sessionReaderImpl struct {
redisClient *redis.Client
}
// newSessionReader returns the concrete reader. Used by NewAuthService to
// embed without going through the SessionReader interface (which would hide
// redisClient from the rest of authServiceImpl).
func newSessionReader(redisClient *redis.Client) *sessionReaderImpl {
return &sessionReaderImpl{redisClient: redisClient}
}
func NewSessionReader(redisClient *redis.Client) SessionReader {
return newSessionReader(redisClient)
}
func (r *sessionReaderImpl) GetSession(ctx context.Context, token string) (*Session, error) {
sessionInfo, err := r.redisClient.Get(ctx, token).Result()
if err != nil {
if errors.Is(err, redis.Nil) {
return nil, ErrSessionNotFound
}
return nil, fmt.Errorf("error getting session: %w", err)
}
var session Session
err = json.Unmarshal([]byte(sessionInfo), &session)
return &session, nil
}
+10 -21
View File
@@ -45,14 +45,14 @@ type Session struct {
}
type AuthService interface {
SessionReader
// RequestSignInCode generates a code and emails it to the provided email.
// To retrieve a session, client must verify with VerifySignInCode.
RequestSignInCode(ctx context.Context, email string) error
// VerifySignInCode returns ErrInvalidCode if incorrect code, otherwise creates a session.
// humanId is stored in the session alongside the email.
VerifySignInCode(ctx context.Context, email, code, humanId string) (sessionToken string, err error)
// GetSession returns ErrSessionNotFound if no valid session
GetSession(ctx context.Context, sessionToken string) (*Session, error)
// ExtendSession returns ErrSessionNotFound if no valid session
ExtendSession(ctx context.Context, sessionToken string) error
SignOut(ctx context.Context, sessionToken string) error
@@ -64,13 +64,17 @@ type AuthService interface {
}
type authServiceImpl struct {
redisClient *redis.Client
aeroSvc pbaero.PrimaryClient
fbAuth *firebaseauth.Client
*sessionReaderImpl
aeroSvc pbaero.PrimaryClient
fbAuth *firebaseauth.Client
}
func NewAuthService(redisClient *redis.Client, aeroSvc pbaero.PrimaryClient, fbAuth *firebaseauth.Client) AuthService {
return &authServiceImpl{redisClient: redisClient, aeroSvc: aeroSvc, fbAuth: fbAuth}
return &authServiceImpl{
sessionReaderImpl: newSessionReader(redisClient),
aeroSvc: aeroSvc,
fbAuth: fbAuth,
}
}
func (a *authServiceImpl) MintFirebaseCustomToken(ctx context.Context, humanId string) (string, error) {
@@ -161,21 +165,6 @@ func (a *authServiceImpl) VerifySignInCode(ctx context.Context, email, code, hum
return token, nil
}
func (a *authServiceImpl) GetSession(ctx context.Context, token string) (*Session, error) {
sessionInfo, err := a.redisClient.Get(ctx, token).Result()
if err != nil {
if errors.Is(err, redis.Nil) {
return nil, ErrSessionNotFound
}
return nil, fmt.Errorf("error getting session: %w", err)
}
var session Session
err = json.Unmarshal([]byte(sessionInfo), &session)
return &session, nil
}
func (a *authServiceImpl) ExtendSession(ctx context.Context, token string) error {
ttl, err := a.redisClient.TTL(ctx, token).Result()
if err != nil {
+2 -2
View File
@@ -18,12 +18,12 @@ type Server struct {
ctx context.Context // server-scoped context for graceful shutdown
hub *Hub
bridge *RedisBridge
authSvc auth.AuthService
authSvc auth.SessionReader
}
// NewServer creates a new pusher server. The ctx controls the lifetime of all
// WebSocket connections — when cancelled, all connections are closed gracefully.
func NewServer(ctx context.Context, hub *Hub, bridge *RedisBridge, authSvc auth.AuthService) *Server {
func NewServer(ctx context.Context, hub *Hub, bridge *RedisBridge, authSvc auth.SessionReader) *Server {
return &Server{
ctx: ctx,
hub: hub,