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 }