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 © <a target="_blank" href="https://ponzu-cms.org">Ponzu</a> | 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: </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 }