github.com/greenpau/go-authcrunch@v1.1.4/pkg/authn/ui/ui.go (about)

     1  // Copyright 2022 Paul Greenberg greenpau@outlook.com
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package ui
    16  
    17  import (
    18  	"bytes"
    19  	"fmt"
    20  	cfgutil "github.com/greenpau/go-authcrunch/pkg/util/cfg"
    21  	"io/ioutil"
    22  	"path"
    23  	"strings"
    24  	"text/template"
    25  )
    26  
    27  // Themes stores UI themes.
    28  var Themes = map[string]interface{}{
    29  	"basic": true,
    30  }
    31  
    32  // Factory represents a collection of HTML templates
    33  // and associated methods for the creation of HTML user interfaces.
    34  type Factory struct {
    35  	Templates               map[string]*Template `json:"templates,omitempty" xml:"templates,omitempty" yaml:"templates,omitempty"`
    36  	Title                   string               `json:"title,omitempty" xml:"title,omitempty" yaml:"title,omitempty"`
    37  	LogoURL                 string               `json:"logo_url,omitempty" xml:"logo_url,omitempty" yaml:"logo_url,omitempty"`
    38  	LogoDescription         string               `json:"logo_description,omitempty" xml:"logo_description,omitempty" yaml:"logo_description,omitempty"`
    39  	MetaTitle               string               `json:"meta_title,omitempty" xml:"meta_title,omitempty" yaml:"meta_title,omitempty"`
    40  	MetaDescription         string               `json:"meta_description,omitempty" xml:"meta_description,omitempty" yaml:"meta_description,omitempty"`
    41  	MetaAuthor              string               `json:"meta_author,omitempty" xml:"meta_author,omitempty" yaml:"meta_author,omitempty"`
    42  	RegistrationEnabled     bool                 `json:"registration_enabled,omitempty" xml:"registration_enabled,omitempty" yaml:"registration_enabled,omitempty"`
    43  	PasswordRecoveryEnabled bool                 `json:"password_recovery_enabled,omitempty" xml:"password_recovery_enabled,omitempty" yaml:"password_recovery_enabled,omitempty"`
    44  	MfaEnabled              bool                 `json:"mfa_enabled,omitempty" xml:"mfa_enabled,omitempty" yaml:"mfa_enabled,omitempty"`
    45  	// The links visible to anonymous user
    46  	PublicLinks []Link `json:"public_links,omitempty" xml:"public_links,omitempty" yaml:"public_links,omitempty"`
    47  	// The links visible to authenticated user
    48  	PrivateLinks []Link `json:"private_links,omitempty" xml:"private_links,omitempty" yaml:"private_links,omitempty"`
    49  	// The authentication realms/domains
    50  	Realms []UserRealm `json:"realms,omitempty" xml:"realms,omitempty" yaml:"realms,omitempty"`
    51  	// The pass to authentication endpoint. This is where
    52  	// user credentials will be passed to via POST.
    53  	ActionEndpoint string `json:"-"`
    54  	CustomCSSPath  string `json:"custom_css_path,omitempty" xml:"custom_css_path,omitempty" yaml:"custom_css_path,omitempty"`
    55  	CustomJsPath   string `json:"custom_js_path,omitempty" xml:"custom_js_path,omitempty" yaml:"custom_js_path,omitempty"`
    56  }
    57  
    58  // Template represents a user interface instance, e.g. a single
    59  // HTML page.
    60  type Template struct {
    61  	Alias string `json:"alias,omitempty" xml:"alias,omitempty" yaml:"alias,omitempty"`
    62  	// Path could be `inline`, URL path, or file path
    63  	Path     string             `json:"path,omitempty" xml:"path,omitempty" yaml:"path,omitempty"`
    64  	Template *template.Template `json:"-"`
    65  }
    66  
    67  // UserRealm represents a single authentication realm/domain.
    68  type UserRealm struct {
    69  	Name  string `json:"name,omitempty" xml:"name,omitempty" yaml:"name,omitempty"`
    70  	Label string `json:"label,omitempty" xml:"label,omitempty" yaml:"label,omitempty"`
    71  }
    72  
    73  // Args is a collection of page attributes
    74  // that needs to be passed to Render method.
    75  type Args struct {
    76  	PageTitle               string                 `json:"page_title,omitempty" xml:"page_title,omitempty" yaml:"page_title,omitempty"`
    77  	NavItems                []*NavigationItem      `json:"nav_items,omitempty" xml:"nav_items,omitempty" yaml:"nav_items,omitempty"`
    78  	LogoURL                 string                 `json:"logo_url,omitempty" xml:"logo_url,omitempty" yaml:"logo_url,omitempty"`
    79  	LogoDescription         string                 `json:"logo_description,omitempty" xml:"logo_description,omitempty" yaml:"logo_description,omitempty"`
    80  	MetaTitle               string                 `json:"meta_title,omitempty" xml:"meta_title,omitempty" yaml:"meta_title,omitempty"`
    81  	MetaDescription         string                 `json:"meta_description,omitempty" xml:"meta_description,omitempty" yaml:"meta_description,omitempty"`
    82  	MetaAuthor              string                 `json:"meta_author,omitempty" xml:"meta_author,omitempty" yaml:"meta_author,omitempty"`
    83  	ActionEndpoint          string                 `json:"action_endpoint,omitempty" xml:"action_endpoint,omitempty" yaml:"action_endpoint,omitempty"`
    84  	Message                 string                 `json:"message,omitempty" xml:"message,omitempty" yaml:"message,omitempty"`
    85  	MessageType             string                 `json:"message_type,omitempty" xml:"message_type,omitempty" yaml:"message_type,omitempty"`
    86  	PublicLinks             []Link                 `json:"public_links,omitempty" xml:"public_links,omitempty" yaml:"public_links,omitempty"`
    87  	PrivateLinks            []Link                 `json:"private_links,omitempty" xml:"private_links,omitempty" yaml:"private_links,omitempty"`
    88  	Realms                  []UserRealm            `json:"realms,omitempty" xml:"realms,omitempty" yaml:"realms,omitempty"`
    89  	Authenticated           bool                   `json:"authenticated,omitempty" xml:"authenticated,omitempty" yaml:"authenticated,omitempty"`
    90  	Data                    map[string]interface{} `json:"data,omitempty" xml:"data,omitempty" yaml:"data,omitempty"`
    91  	RegistrationEnabled     bool                   `json:"registration_enabled,omitempty" xml:"registration_enabled,omitempty" yaml:"registration_enabled,omitempty"`
    92  	PasswordRecoveryEnabled bool                   `json:"password_recovery_enabled,omitempty" xml:"password_recovery_enabled,omitempty" yaml:"password_recovery_enabled,omitempty"`
    93  	MfaEnabled              bool                   `json:"mfa_enabled,omitempty" xml:"mfa_enabled,omitempty" yaml:"mfa_enabled,omitempty"`
    94  	CustomCSSEnabled        bool                   `json:"custom_css_enabled,omitempty" xml:"custom_css_enabled,omitempty" yaml:"custom_css_enabled,omitempty"`
    95  	CustomJsEnabled         bool                   `json:"custom_js_enabled,omitempty" xml:"custom_js_enabled,omitempty" yaml:"custom_js_enabled,omitempty"`
    96  }
    97  
    98  // NewFactory return an instance of a user interface factory.
    99  func NewFactory() *Factory {
   100  	return &Factory{
   101  		LogoURL:         "/assets/images/logo.svg",
   102  		LogoDescription: "Authentication Portal",
   103  		Templates:       make(map[string]*Template),
   104  		PublicLinks:     []Link{},
   105  		PrivateLinks:    []Link{},
   106  		Realms:          []UserRealm{},
   107  	}
   108  }
   109  
   110  // NewTemplate returns a user interface template
   111  func NewTemplate(s, tp string) (*Template, error) {
   112  	var templateBody string
   113  	if s == "" {
   114  		return nil, fmt.Errorf("the user interface alias cannot be empty")
   115  	}
   116  	if tp == "" {
   117  		return nil, fmt.Errorf("the path to user interface template cannot be empty")
   118  	}
   119  	tmpl := &Template{
   120  		Alias: s,
   121  		Path:  tp,
   122  	}
   123  
   124  	if tp == "inline" {
   125  		if _, exists := PageTemplates[s]; !exists {
   126  			return nil, fmt.Errorf("built-in template does not exists: %s", s)
   127  		}
   128  		templateBody = PageTemplates[s]
   129  	} else {
   130  		if strings.HasPrefix(tp, "http://") || strings.HasPrefix(tp, "https://") {
   131  			return nil, fmt.Errorf("the loading of template from remote URL is not supported yet")
   132  		}
   133  		// Assuming the template is a file system template
   134  		content, err := ioutil.ReadFile(tp)
   135  		if err != nil {
   136  			return nil, fmt.Errorf("failed to load %s template from %s: %s", s, tp, err)
   137  		}
   138  		templateBody = string(content)
   139  	}
   140  
   141  	t, err := loadTemplateFromString(s, templateBody)
   142  	if err != nil {
   143  		return nil, fmt.Errorf("Failed to load %s template from %s: %s", s, tp, err)
   144  	}
   145  	tmpl.Template = t
   146  	return tmpl, nil
   147  }
   148  
   149  // GetArgs return an instance of Args. Upon the receipt
   150  // of the arguments, they can be manipulated and passed to
   151  // Factory.Render method. The manipulation means
   152  // adding an error message, appending to the title of a page,
   153  // adding arbitrary data etc.
   154  func (f *Factory) GetArgs() *Args {
   155  	args := &Args{
   156  		PageTitle:               f.Title,
   157  		LogoURL:                 f.LogoURL,
   158  		LogoDescription:         f.LogoDescription,
   159  		MetaTitle:               f.MetaTitle,
   160  		MetaDescription:         f.MetaDescription,
   161  		MetaAuthor:              f.MetaAuthor,
   162  		PublicLinks:             f.PublicLinks,
   163  		PrivateLinks:            f.PrivateLinks,
   164  		Realms:                  f.Realms,
   165  		ActionEndpoint:          f.ActionEndpoint,
   166  		Data:                    make(map[string]interface{}),
   167  		RegistrationEnabled:     f.RegistrationEnabled,
   168  		PasswordRecoveryEnabled: f.PasswordRecoveryEnabled,
   169  		MfaEnabled:              f.MfaEnabled,
   170  	}
   171  	uiOptions := make(map[string]interface{})
   172  	if f.CustomCSSPath != "" {
   173  		args.CustomCSSEnabled = true
   174  		uiOptions["custom_css_required"] = "yes"
   175  	} else {
   176  		uiOptions["custom_css_required"] = "no"
   177  	}
   178  
   179  	if f.CustomJsPath != "" {
   180  		args.CustomJsEnabled = true
   181  		uiOptions["custom_js_required"] = "yes"
   182  	} else {
   183  		uiOptions["custom_js_required"] = "no"
   184  	}
   185  	args.Data["ui_options"] = uiOptions
   186  	return args
   187  }
   188  
   189  // BaseURL sets base URL for the authentication portal.
   190  func (args *Args) BaseURL(s string) {
   191  	if !strings.HasPrefix(args.LogoURL, "http") {
   192  		args.LogoURL = path.Join(s, args.LogoURL)
   193  	}
   194  	args.ActionEndpoint = s
   195  }
   196  
   197  // AddFrontendLinks adds private links.
   198  func (args *Args) AddFrontendLinks(arr []string) {
   199  	for _, encodedArgs := range arr {
   200  		parts, err := cfgutil.DecodeArgs(encodedArgs)
   201  		if err != nil {
   202  			continue
   203  		}
   204  		lnk := Link{
   205  			Title: parts[0],
   206  			Link:  parts[1],
   207  		}
   208  		argp := 2
   209  		var disabledLink bool
   210  		for argp < len(parts) {
   211  			switch parts[argp] {
   212  			case "target_blank":
   213  				lnk.Target = "_blank"
   214  				lnk.TargetEnabled = true
   215  			case "icon":
   216  				argp++
   217  				if argp < len(parts) {
   218  					lnk.IconName = parts[argp]
   219  					lnk.IconEnabled = true
   220  				}
   221  			case "disabled":
   222  				disabledLink = true
   223  				break
   224  			}
   225  			argp++
   226  		}
   227  		if disabledLink {
   228  			continue
   229  		}
   230  		args.PrivateLinks = append(args.PrivateLinks, lnk)
   231  	}
   232  }
   233  
   234  // AddBuiltinTemplates adds all built-in template to Factory
   235  func (f *Factory) AddBuiltinTemplates() error {
   236  	for name := range PageTemplates {
   237  		if err := f.AddBuiltinTemplate(name); err != nil {
   238  			return fmt.Errorf("Failed to load built-in template %s: %s", name, err)
   239  		}
   240  	}
   241  	return nil
   242  }
   243  
   244  // AddBuiltinTemplate adds a built-in template to Factory
   245  func (f *Factory) AddBuiltinTemplate(name string) error {
   246  	if _, exists := f.Templates[name]; exists {
   247  		return fmt.Errorf("template %s already defined", name)
   248  	}
   249  	if _, exists := PageTemplates[name]; !exists {
   250  		return fmt.Errorf("built-in template %s does not exists", name)
   251  	}
   252  	tmpl, err := NewTemplate(name, "inline")
   253  	if err != nil {
   254  		return err
   255  	}
   256  	f.Templates[name] = tmpl
   257  	return nil
   258  }
   259  
   260  // AddTemplate adds a template to Factory.
   261  func (f *Factory) AddTemplate(s, tp string) error {
   262  	if _, exists := f.Templates[s]; exists {
   263  		return fmt.Errorf("Template already defined: %s", s)
   264  	}
   265  	tmpl, err := NewTemplate(s, tp)
   266  	if err != nil {
   267  		return err
   268  	}
   269  	f.Templates[s] = tmpl
   270  	return nil
   271  }
   272  
   273  // DeleteTemplates removes all templates from Factory.
   274  func (f *Factory) DeleteTemplates() {
   275  	f.Templates = make(map[string]*Template)
   276  	return
   277  }
   278  
   279  func loadTemplateFromString(s, p string) (*template.Template, error) {
   280  	funcMap := template.FuncMap{
   281  		"pathjoin": path.Join,
   282  		"brsplitline": func(s string) string {
   283  			var output []rune
   284  			var count = 0
   285  			for _, c := range s {
   286  				count++
   287  				if count > 25 {
   288  					count = 0
   289  					output = append(output, []rune{'<', 'b', 'r', '>'}...)
   290  				}
   291  				output = append(output, c)
   292  			}
   293  			return string(output)
   294  		},
   295  	}
   296  	t := template.New(s).Funcs(funcMap)
   297  	t, err := t.Parse(p)
   298  	if err != nil {
   299  		return nil, err
   300  	}
   301  	return t, nil
   302  }
   303  
   304  // Render returns a pointer to a data buffer.
   305  func (f *Factory) Render(name string, args *Args) (*bytes.Buffer, error) {
   306  	if _, exists := f.Templates[name]; !exists {
   307  		return nil, fmt.Errorf("template %s does not exist", name)
   308  	}
   309  	b := bytes.NewBuffer(nil)
   310  	err := f.Templates[name].Template.Execute(b, args)
   311  	if err != nil {
   312  		return nil, err
   313  	}
   314  	return b, nil
   315  }