package pusher import ( "context" "errors" "strings" "github.com/flowy-live/llink/internal/network" ) var ErrUnauthorized = errors.New("unauthorized") // Authorizer validates whether a user can access a given channel. type Authorizer struct { networkSvc network.Service } // NewAuthorizer creates a new channel authorizer. func NewAuthorizer(networkSvc network.Service) *Authorizer { return &Authorizer{networkSvc: networkSvc} } // Authorize checks if the given humanID is allowed to subscribe to the channel. // Channel formats: // - network:{networkId} // - stream:{networkId}:{streamId} func (a *Authorizer) Authorize(ctx context.Context, channelID, humanID string) error { parts := strings.SplitN(channelID, ":", 2) if len(parts) < 2 { return ErrUnauthorized } channelType := parts[0] rest := parts[1] switch channelType { case "network": return a.authorizeNetwork(ctx, rest, humanID) case "stream": return a.authorizeStream(ctx, rest, humanID) case "_presence": // Always allowed — used for global online presence tracking. // The channel ID is _presence:{humanId}, so verify the humanId matches. if rest != humanID { return ErrUnauthorized } return nil default: return ErrUnauthorized } } func (a *Authorizer) authorizeNetwork(ctx context.Context, networkID, humanID string) error { isMember, err := a.networkSvc.IsMember(ctx, networkID, humanID) if err != nil { return err } if !isMember { return ErrUnauthorized } return nil } // authorizeStream expects rest to be "{networkId}:{streamId}". // We only check network membership — stream visibility is handled by network access. func (a *Authorizer) authorizeStream(ctx context.Context, rest, humanID string) error { parts := strings.SplitN(rest, ":", 2) if len(parts) < 2 { return ErrUnauthorized } networkID := parts[0] return a.authorizeNetwork(ctx, networkID, humanID) }