github.com/bosssauce/ponzu@v0.11.1-0.20200102001432-9bc41b703131/system/admin/admin.go (about)

     1  // Package admin desrcibes the admin view containing references to
     2  // various managers and editors
     3  package admin
     4  
     5  import (
     6  	"bytes"
     7  	"encoding/json"
     8  	"fmt"
     9  	"html/template"
    10  	"net/http"
    11  
    12  	"github.com/ponzu-cms/ponzu/system/admin/user"
    13  	"github.com/ponzu-cms/ponzu/system/api/analytics"
    14  	"github.com/ponzu-cms/ponzu/system/db"
    15  	"github.com/ponzu-cms/ponzu/system/item"
    16  )
    17  
    18  var startAdminHTML = `<!doctype html>
    19  <html lang="en">
    20      <head>
    21          <title>{{ .Logo }}</title>
    22          <script type="text/javascript" src="/admin/static/common/js/jquery-2.1.4.min.js"></script>
    23          <script type="text/javascript" src="/admin/static/common/js/util.js"></script>
    24          <script type="text/javascript" src="/admin/static/dashboard/js/materialize.min.js"></script>
    25          <script type="text/javascript" src="/admin/static/dashboard/js/chart.bundle.min.js"></script>
    26          <script type="text/javascript" src="/admin/static/editor/js/materialNote.js"></script> 
    27          <script type="text/javascript" src="/admin/static/editor/js/ckMaterializeOverrides.js"></script>
    28                    
    29          <link rel="stylesheet" href="/admin/static/dashboard/css/material-icons.css" />     
    30          <link rel="stylesheet" href="/admin/static/dashboard/css/materialize.min.css" />
    31          <link rel="stylesheet" href="/admin/static/editor/css/materialNote.css" />
    32          <link rel="stylesheet" href="/admin/static/dashboard/css/admin.css" />    
    33  
    34          <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    35          <meta charset="utf-8">
    36          <meta http-equiv="X-UA-Compatible" content="IE=edge">
    37      </head>
    38      <body class="grey lighten-4">
    39         <div class="navbar-fixed">
    40              <nav class="grey darken-2">
    41              <div class="nav-wrapper">
    42                  <a class="brand-logo" href="/admin">{{ .Logo }}</a>
    43  
    44                  <ul class="right">
    45                      <li><a href="/admin/logout">Logout</a></li>
    46                  </ul>
    47              </div>
    48              </nav>
    49          </div>
    50  
    51          <div class="admin-ui row">`
    52  
    53  var mainAdminHTML = `
    54              <div class="left-nav col s3">
    55                  <div class="card">
    56                  <ul class="card-content collection">
    57                      <div class="card-title">Content</div>
    58                                      
    59                      {{ range $t, $f := .Types }}
    60                      <div class="row collection-item">
    61                          <li><a class="col s12" href="/admin/contents?type={{ $t }}"><i class="tiny left material-icons">playlist_add</i>{{ $t }}</a></li>
    62                      </div>
    63                      {{ end }}
    64  
    65                      <div class="card-title">System</div>                                
    66                      <div class="row collection-item">
    67                          <li><a class="col s12" href="/admin/configure"><i class="tiny left material-icons">settings</i>Configuration</a></li>
    68                          <li><a class="col s12" href="/admin/configure/users"><i class="tiny left material-icons">supervisor_account</i>Admin Users</a></li>
    69                          <li><a class="col s12" href="/admin/uploads"><i class="tiny left material-icons">swap_vert</i>Uploads</a></li>
    70                          <li><a class="col s12" href="/admin/addons"><i class="tiny left material-icons">settings_input_svideo</i>Addons</a></li>
    71                      </div>
    72                  </ul>
    73                  </div>
    74              </div>
    75              {{ if .Subview}}
    76              <div class="subview col s9">
    77                  {{ .Subview }}
    78              </div>
    79              {{ end }}`
    80  
    81  var endAdminHTML = `
    82          </div>
    83          <footer class="row">
    84              <div class="col s12">
    85                  <p class="center-align">Powered by &copy; <a target="_blank" href="https://ponzu-cms.org">Ponzu</a> &nbsp;&vert;&nbsp; open-sourced by <a target="_blank" href="https://www.bosssauce.it">Boss Sauce Creative</a></p>
    86              </div>     
    87          </footer>
    88      </body>
    89  </html>`
    90  
    91  type admin struct {
    92  	Logo    string
    93  	Types   map[string]func() interface{}
    94  	Subview template.HTML
    95  }
    96  
    97  // Admin ...
    98  func Admin(view []byte) (_ []byte, err error) {
    99  	cfg, err := db.Config("name")
   100  	if err != nil {
   101  		return
   102  	}
   103  
   104  	if cfg == nil {
   105  		cfg = []byte("")
   106  	}
   107  
   108  	a := admin{
   109  		Logo:    string(cfg),
   110  		Types:   item.Types,
   111  		Subview: template.HTML(view),
   112  	}
   113  
   114  	buf := &bytes.Buffer{}
   115  	html := startAdminHTML + mainAdminHTML + endAdminHTML
   116  	tmpl := template.Must(template.New("admin").Parse(html))
   117  	err = tmpl.Execute(buf, a)
   118  	if err != nil {
   119  		return
   120  	}
   121  
   122  	return buf.Bytes(), nil
   123  }
   124  
   125  var initAdminHTML = `
   126  <div class="init col s5">
   127  <div class="card">
   128  <div class="card-content">
   129      <div class="card-title">Welcome!</div>
   130      <blockquote>You need to initialize your system by filling out the form below. All of 
   131      this information can be updated later on, but you will not be able to start 
   132      without first completing this step.</blockquote>
   133      <form method="post" action="/admin/init" class="row">
   134          <div>Configuration</div>
   135          <div class="input-field col s12">        
   136              <input placeholder="Enter the name of your site (interal use only)" class="validate required" type="text" id="name" name="name"/>
   137              <label for="name" class="active">Site Name</label>
   138          </div>
   139          <div class="input-field col s12">        
   140              <input placeholder="Used for acquiring SSL certificate (e.g. www.example.com or  example.com)" class="validate" type="text" id="domain" name="domain"/>
   141              <label for="domain" class="active">Domain</label>
   142          </div>
   143          <div>Admin Details</div>
   144          <div class="input-field col s12">
   145              <input placeholder="Your email address e.g. you@example.com" class="validate required" type="email" id="email" name="email"/>
   146              <label for="email" class="active">Email</label>
   147          </div>
   148          <div class="input-field col s12">
   149              <input placeholder="Enter a strong password" class="validate required" type="password" id="password" name="password"/>
   150              <label for="password" class="active">Password</label>        
   151          </div>
   152          <button class="btn waves-effect waves-light right">Start</button>
   153      </form>
   154  </div>
   155  </div>
   156  </div>
   157  <script>
   158      $(function() {
   159          $('.nav-wrapper ul.right').hide();
   160          
   161          var logo = $('a.brand-logo');
   162          var name = $('input#name');
   163          var domain = $('input#domain');
   164          var hostname = domain.val();
   165  
   166          if (hostname === '') {    
   167              hostname = window.location.host || window.location.hostname;
   168          }
   169          
   170          if (hostname.indexOf(':') !== -1) {
   171              hostname = hostname.split(':')[0];
   172          }
   173          
   174          domain.val(hostname);
   175          
   176          name.on('change', function(e) {
   177              logo.text(e.target.value);
   178          });
   179  
   180      });
   181  </script>
   182  `
   183  
   184  // Init ...
   185  func Init() ([]byte, error) {
   186  	html := startAdminHTML + initAdminHTML + endAdminHTML
   187  
   188  	name, err := db.Config("name")
   189  	if err != nil {
   190  		return nil, err
   191  	}
   192  
   193  	if name == nil {
   194  		name = []byte("")
   195  	}
   196  
   197  	a := admin{
   198  		Logo: string(name),
   199  	}
   200  
   201  	buf := &bytes.Buffer{}
   202  	tmpl := template.Must(template.New("init").Parse(html))
   203  	err = tmpl.Execute(buf, a)
   204  	if err != nil {
   205  		return nil, err
   206  	}
   207  
   208  	return buf.Bytes(), nil
   209  }
   210  
   211  var loginAdminHTML = `
   212  <div class="init col s5">
   213  <div class="card">
   214  <div class="card-content">
   215      <div class="card-title">Welcome!</div>
   216      <blockquote>Please log in to the system using your email address and password.</blockquote>
   217      <form method="post" action="/admin/login" class="row">
   218          <div class="input-field col s12">
   219              <input placeholder="Enter your email address e.g. you@example.com" class="validate required" type="email" id="email" name="email"/>
   220              <label for="email" class="active">Email</label>
   221          </div>
   222          <div class="input-field col s12">
   223              <input placeholder="Enter your password" class="validate required" type="password" id="password" name="password"/>
   224              <a href="/admin/recover">Forgot password?</a>            
   225              <label for="password" class="active">Password</label>  
   226          </div>
   227          <button class="btn waves-effect waves-light right">Log in</button>
   228      </form>
   229  </div>
   230  </div>
   231  </div>
   232  <script>
   233      $(function() {
   234          $('.nav-wrapper ul.right').hide();
   235      });
   236  </script>
   237  `
   238  
   239  // Login ...
   240  func Login() ([]byte, error) {
   241  	html := startAdminHTML + loginAdminHTML + endAdminHTML
   242  
   243  	cfg, err := db.Config("name")
   244  	if err != nil {
   245  		return nil, err
   246  	}
   247  
   248  	if cfg == nil {
   249  		cfg = []byte("")
   250  	}
   251  
   252  	a := admin{
   253  		Logo: string(cfg),
   254  	}
   255  
   256  	buf := &bytes.Buffer{}
   257  	tmpl := template.Must(template.New("login").Parse(html))
   258  	err = tmpl.Execute(buf, a)
   259  	if err != nil {
   260  		return nil, err
   261  	}
   262  
   263  	return buf.Bytes(), nil
   264  }
   265  
   266  var forgotPasswordHTML = `
   267  <div class="init col s5">
   268  <div class="card">
   269  <div class="card-content">
   270      <div class="card-title">Account Recovery</div>
   271      <blockquote>Please enter the email for your account and a recovery message will be sent to you at this address. Check your spam folder in case the message was flagged.</blockquote>
   272      <form method="post" action="/admin/recover" class="row" enctype="multipart/form-data">
   273          <div class="input-field col s12">
   274              <input placeholder="Enter your email address e.g. you@example.com" class="validate required" type="email" id="email" name="email"/>
   275              <label for="email" class="active">Email</label>
   276          </div>
   277          
   278          <a href="/admin/recover/key">Already have a recovery key?</a>
   279          <button class="btn waves-effect waves-light right">Send Recovery Email</button>
   280      </form>
   281  </div>
   282  </div>
   283  </div>
   284  <script>
   285      $(function() {
   286          $('.nav-wrapper ul.right').hide();
   287      });
   288  </script>
   289  `
   290  
   291  // ForgotPassword ...
   292  func ForgotPassword() ([]byte, error) {
   293  	html := startAdminHTML + forgotPasswordHTML + endAdminHTML
   294  
   295  	cfg, err := db.Config("name")
   296  	if err != nil {
   297  		return nil, err
   298  	}
   299  
   300  	if cfg == nil {
   301  		cfg = []byte("")
   302  	}
   303  
   304  	a := admin{
   305  		Logo: string(cfg),
   306  	}
   307  
   308  	buf := &bytes.Buffer{}
   309  	tmpl := template.Must(template.New("forgotPassword").Parse(html))
   310  	err = tmpl.Execute(buf, a)
   311  	if err != nil {
   312  		return nil, err
   313  	}
   314  
   315  	return buf.Bytes(), nil
   316  }
   317  
   318  var recoveryKeyHTML = `
   319  <div class="init col s5">
   320  <div class="card">
   321  <div class="card-content">
   322      <div class="card-title">Account Recovery</div>
   323      <blockquote>Please check for your recovery key inside an email sent to the address you provided. Check your spam folder in case the message was flagged.</blockquote>
   324      <form method="post" action="/admin/recover/key" class="row" enctype="multipart/form-data">
   325          <div class="input-field col s12">
   326              <input placeholder="Enter your recovery key" class="validate required" type="text" id="key" name="key"/>
   327              <label for="key" class="active">Recovery Key</label>
   328          </div>
   329  
   330          <div class="input-field col s12">
   331              <input placeholder="Enter your email address e.g. you@example.com" class="validate required" type="email" id="email" name="email"/>
   332              <label for="email" class="active">Email</label>
   333          </div>
   334  
   335          <div class="input-field col s12">
   336              <input placeholder="Enter your password" class="validate required" type="password" id="password" name="password"/>
   337              <label for="password" class="active">New Password</label>
   338          </div>
   339          
   340          <button class="btn waves-effect waves-light right">Update Account</button>
   341      </form>
   342  </div>
   343  </div>
   344  </div>
   345  <script>
   346      $(function() {
   347          $('.nav-wrapper ul.right').hide();
   348      });
   349  </script>
   350  `
   351  
   352  // RecoveryKey ...
   353  func RecoveryKey() ([]byte, error) {
   354  	html := startAdminHTML + recoveryKeyHTML + endAdminHTML
   355  
   356  	cfg, err := db.Config("name")
   357  	if err != nil {
   358  		return nil, err
   359  	}
   360  
   361  	if cfg == nil {
   362  		cfg = []byte("")
   363  	}
   364  
   365  	a := admin{
   366  		Logo: string(cfg),
   367  	}
   368  
   369  	buf := &bytes.Buffer{}
   370  	tmpl := template.Must(template.New("recoveryKey").Parse(html))
   371  	err = tmpl.Execute(buf, a)
   372  	if err != nil {
   373  		return nil, err
   374  	}
   375  
   376  	return buf.Bytes(), nil
   377  }
   378  
   379  // UsersList ...
   380  func UsersList(req *http.Request) ([]byte, error) {
   381  	html := `
   382      <div class="card user-management">
   383          <div class="card-title">Edit your account:</div>    
   384          <form class="row" enctype="multipart/form-data" action="/admin/configure/users/edit" method="post">
   385              <div class="col s9">
   386                  <label class="active">Email Address</label>
   387                  <input type="email" name="email" value="{{ .User.Email }}"/>
   388              </div>
   389  
   390              <div class="col s9">
   391                  <div>To approve changes, enter your password:</div>
   392                  
   393                  <label class="active">Current Password</label>
   394                  <input type="password" name="password"/>
   395              </div>
   396  
   397              <div class="col s9">
   398                  <label class="active">New Password: (leave blank if no password change needed)</label>
   399                  <input name="new_password" type="password"/>
   400              </div>
   401  
   402              <div class="col s9">                        
   403                  <button class="btn waves-effect waves-light green right" type="submit">Save</button>
   404              </div>
   405          </form>
   406  
   407          <div class="card-title">Add a new user:</div>        
   408          <form class="row" enctype="multipart/form-data" action="/admin/configure/users" method="post">
   409              <div class="col s9">
   410                  <label class="active">Email Address</label>
   411                  <input type="email" name="email" value=""/>
   412              </div>
   413  
   414              <div class="col s9">
   415                  <label class="active">Password</label>
   416                  <input type="password" name="password"/>
   417              </div>
   418  
   419              <div class="col s9">            
   420                  <button class="btn waves-effect waves-light green right" type="submit">Add User</button>
   421              </div>   
   422          </form>        
   423  
   424          <div class="card-title">Remove Admin Users</div>        
   425          <ul class="users row">
   426              {{ range .Users }}
   427              <li class="col s9">
   428                  {{ .Email }}
   429                  <form enctype="multipart/form-data" class="delete-user __ponzu right" action="/admin/configure/users/delete" method="post">
   430                      <span>Delete</span>
   431                      <input type="hidden" name="email" value="{{ .Email }}"/>
   432                      <input type="hidden" name="id" value="{{ .ID }}"/>
   433                  </form>
   434              </li>
   435              {{ end }}
   436          </ul>
   437      </div>
   438      `
   439  	script := `
   440      <script>
   441          $(function() {
   442              var del = $('.delete-user.__ponzu span');
   443              del.on('click', function(e) {
   444                  if (confirm("[Ponzu] Please confirm:\n\nAre you sure you want to delete this user?\nThis cannot be undone.")) {
   445                      $(e.target).parent().submit();
   446                  }
   447              });
   448          });
   449      </script>
   450      `
   451  	// get current user out to pass as data to execute template
   452  	j, err := db.CurrentUser(req)
   453  	if err != nil {
   454  		return nil, err
   455  	}
   456  
   457  	var usr user.User
   458  	err = json.Unmarshal(j, &usr)
   459  	if err != nil {
   460  		return nil, err
   461  	}
   462  
   463  	// get all users to list
   464  	jj, err := db.UserAll()
   465  	if err != nil {
   466  		return nil, err
   467  	}
   468  
   469  	var usrs []user.User
   470  	for i := range jj {
   471  		var u user.User
   472  		err = json.Unmarshal(jj[i], &u)
   473  		if err != nil {
   474  			return nil, err
   475  		}
   476  		if u.Email != usr.Email {
   477  			usrs = append(usrs, u)
   478  		}
   479  	}
   480  
   481  	// make buffer to execute html into then pass buffer's bytes to Admin
   482  	buf := &bytes.Buffer{}
   483  	tmpl := template.Must(template.New("users").Parse(html + script))
   484  	data := map[string]interface{}{
   485  		"User":  usr,
   486  		"Users": usrs,
   487  	}
   488  
   489  	err = tmpl.Execute(buf, data)
   490  	if err != nil {
   491  		return nil, err
   492  	}
   493  
   494  	return Admin(buf.Bytes())
   495  }
   496  
   497  var analyticsHTML = `
   498  <div class="analytics">
   499  <div class="card">
   500  <div class="card-content">
   501      <p class="right">Data range: {{ .from }} - {{ .to }} (UTC)</p>
   502      <div class="card-title">API Requests</div>
   503      <canvas id="analytics-chart"></canvas>
   504      <script>
   505      var target = document.getElementById("analytics-chart");
   506      Chart.defaults.global.defaultFontColor = '#212121';
   507      Chart.defaults.global.defaultFontFamily = "'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', 'sans-serif'";
   508      Chart.defaults.global.title.position = 'right';
   509      var chart = new Chart(target, {
   510          type: 'bar',
   511          data: {
   512              labels: [{{ range $date := .dates }} "{{ $date }}",  {{ end }}],
   513              datasets: [{
   514                  type: 'line',
   515                  label: 'Unique Clients',
   516                  data: $.parseJSON({{ .unique }}),
   517                  backgroundColor: 'rgba(76, 175, 80, 0.2)',
   518                  borderColor: 'rgba(76, 175, 80, 1)',
   519                  borderWidth: 1
   520              },
   521              {
   522                  type: 'bar',
   523                  label: 'Total Requests',
   524                  data: $.parseJSON({{ .total }}),
   525                  backgroundColor: 'rgba(33, 150, 243, 0.2)',
   526                  borderColor: 'rgba(33, 150, 243, 1)',
   527                  borderWidth: 1
   528              }]
   529          },
   530          options: {
   531              scales: {
   532                  yAxes: [{
   533                      ticks: {
   534                          beginAtZero:true
   535                      }
   536                  }]
   537              }
   538          }
   539      });
   540      </script>
   541  </div>
   542  </div>
   543  </div>
   544  `
   545  
   546  // Dashboard returns the admin view with analytics dashboard
   547  func Dashboard() ([]byte, error) {
   548  	buf := &bytes.Buffer{}
   549  	data, err := analytics.ChartData()
   550  	if err != nil {
   551  		return nil, err
   552  	}
   553  
   554  	tmpl := template.Must(template.New("analytics").Parse(analyticsHTML))
   555  	err = tmpl.Execute(buf, data)
   556  	if err != nil {
   557  		return nil, err
   558  	}
   559  	return Admin(buf.Bytes())
   560  }
   561  
   562  var err400HTML = []byte(`
   563  <div class="error-page e400 col s6">
   564  <div class="card">
   565  <div class="card-content">
   566      <div class="card-title"><b>400</b> Error: Bad Request</div>
   567      <blockquote>Sorry, the request was unable to be completed.</blockquote>
   568  </div>
   569  </div>
   570  </div>
   571  `)
   572  
   573  // Error400 creates a subview for a 400 error page
   574  func Error400() ([]byte, error) {
   575  	return Admin(err400HTML)
   576  }
   577  
   578  var err404HTML = []byte(`
   579  <div class="error-page e404 col s6">
   580  <div class="card">
   581  <div class="card-content">
   582      <div class="card-title"><b>404</b> Error: Not Found</div>
   583      <blockquote>Sorry, the page you requested could not be found.</blockquote>
   584  </div>
   585  </div>
   586  </div>
   587  `)
   588  
   589  // Error404 creates a subview for a 404 error page
   590  func Error404() ([]byte, error) {
   591  	return Admin(err404HTML)
   592  }
   593  
   594  var err405HTML = []byte(`
   595  <div class="error-page e405 col s6">
   596  <div class="card">
   597  <div class="card-content">
   598      <div class="card-title"><b>405</b> Error: Method Not Allowed</div>
   599      <blockquote>Sorry, the method of your request is not allowed.</blockquote>
   600  </div>
   601  </div>
   602  </div>
   603  `)
   604  
   605  // Error405 creates a subview for a 405 error page
   606  func Error405() ([]byte, error) {
   607  	return Admin(err405HTML)
   608  }
   609  
   610  var err500HTML = []byte(`
   611  <div class="error-page e500 col s6">
   612  <div class="card">
   613  <div class="card-content">
   614      <div class="card-title"><b>500</b> Error: Internal Service Error</div>
   615      <blockquote>Sorry, something unexpectedly went wrong.</blockquote>
   616  </div>
   617  </div>
   618  </div>
   619  `)
   620  
   621  // Error500 creates a subview for a 500 error page
   622  func Error500() ([]byte, error) {
   623  	return Admin(err500HTML)
   624  }
   625  
   626  var errMessageHTML = `
   627  <div class="error-page eMsg col s6">
   628  <div class="card">
   629  <div class="card-content">
   630      <div class="card-title"><b>Error:&nbsp;</b>%s</div>
   631      <blockquote>%s</blockquote>
   632  </div>
   633  </div>
   634  </div>
   635  `
   636  
   637  // ErrorMessage is a generic error message container, similar to Error500() and
   638  // others in this package, ecxept it expects the caller to provide a title and
   639  // message to describe to a view why the error is being shown
   640  func ErrorMessage(title, message string) ([]byte, error) {
   641  	eHTML := fmt.Sprintf(errMessageHTML, title, message)
   642  	return Admin([]byte(eHTML))
   643  }