Initial "Bluez" default theme and Admin Setup page
This commit is contained in:
parent
0c243c849c
commit
456cad7a50
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,2 +1,3 @@
|
|||
bin/
|
||||
dist/
|
||||
root/.private
|
||||
|
|
10
core/admin.go
Normal file
10
core/admin.go
Normal file
|
@ -0,0 +1,10 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// SetupHandler is the initial blog setup route.
|
||||
func (b *Blog) SetupHandler(w http.ResponseWriter, r *http.Request) {
|
||||
b.RenderTemplate(w, r, "admin/setup", nil)
|
||||
}
|
|
@ -10,6 +10,8 @@ import (
|
|||
// Blog is the root application object that maintains the app configuration
|
||||
// and helper objects.
|
||||
type Blog struct {
|
||||
Debug bool
|
||||
|
||||
// DocumentRoot is the core static files root; UserRoot masks over it.
|
||||
DocumentRoot string
|
||||
UserRoot string
|
||||
|
@ -27,6 +29,7 @@ func New(documentRoot, userRoot string) *Blog {
|
|||
}
|
||||
r := mux.NewRouter()
|
||||
blog.r = r
|
||||
r.HandleFunc("/admin/setup", blog.SetupHandler)
|
||||
r.HandleFunc("/", blog.PageHandler)
|
||||
r.NotFoundHandler = http.HandlerFunc(blog.PageHandler)
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
@ -13,13 +14,64 @@ func (b *Blog) PageHandler(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
// Remove trailing slashes by redirecting them away.
|
||||
if len(path) > 1 && path[len(path)-1] == '/' {
|
||||
Redirect(w, strings.TrimRight(path, "/"))
|
||||
b.Redirect(w, strings.TrimRight(path, "/"))
|
||||
return
|
||||
}
|
||||
|
||||
// Restrict special paths.
|
||||
if strings.HasPrefix(strings.ToLower(path), "/.") {
|
||||
b.Forbidden(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Search for a file that matches their URL.
|
||||
filepath, err := b.ResolvePath(path)
|
||||
if err != nil {
|
||||
b.NotFound(w, r, "The page you were looking for was not found.")
|
||||
return
|
||||
}
|
||||
|
||||
// Is it a template file?
|
||||
if strings.HasSuffix(filepath.URI, ".gohtml") || strings.HasSuffix(filepath.URI, ".html") {
|
||||
b.RenderTemplate(w, r, filepath.URI, nil)
|
||||
return
|
||||
}
|
||||
|
||||
http.ServeFile(w, r, filepath.Absolute)
|
||||
}
|
||||
|
||||
// Filepath represents a file discovered in the document roots, and maintains
|
||||
// both its relative and absolute components.
|
||||
type Filepath struct {
|
||||
// Canonicalized URI version of the file resolved on disk,
|
||||
// possible with a file extension injected.
|
||||
// (i.e. "/about" -> "about.html")
|
||||
URI string
|
||||
Relative string // Relative path including document root (i.e. "root/about.html")
|
||||
Absolute string // Absolute path on disk (i.e. "/opt/blog/root/about.html")
|
||||
}
|
||||
|
||||
func (f Filepath) String() string {
|
||||
return f.Relative
|
||||
}
|
||||
|
||||
// ResolvePath matches a filesystem path to a relative request URI.
|
||||
//
|
||||
// This checks the UserRoot first and then the DocumentRoot. This way the user
|
||||
// may override templates from the core app's document root.
|
||||
func (b *Blog) ResolvePath(path string) (Filepath, error) {
|
||||
// Strip leading slashes.
|
||||
if path[0] == '/' {
|
||||
path = strings.TrimPrefix(path, "/")
|
||||
}
|
||||
|
||||
log.Debug("Resolving filepath for URI: %s", path)
|
||||
for _, root := range []string{b.DocumentRoot, b.UserRoot} {
|
||||
if len(root) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Resolve the file path.
|
||||
relPath := filepath.Join(root, path)
|
||||
absPath, err := filepath.Abs(relPath)
|
||||
if err != nil {
|
||||
|
@ -31,26 +83,26 @@ func (b *Blog) PageHandler(w http.ResponseWriter, r *http.Request) {
|
|||
// Found an exact hit?
|
||||
if stat, err := os.Stat(absPath); !os.IsNotExist(err) && !stat.IsDir() {
|
||||
log.Debug("Exact filepath found: %s", absPath)
|
||||
http.ServeFile(w, r, absPath)
|
||||
return
|
||||
return Filepath{path, relPath, absPath}, nil
|
||||
}
|
||||
|
||||
// Try some supported suffixes.
|
||||
suffixes := []string{
|
||||
".gohtml",
|
||||
".html",
|
||||
"/index.gohtml",
|
||||
"/index.html",
|
||||
".md",
|
||||
"/index.md",
|
||||
}
|
||||
for _, suffix := range suffixes {
|
||||
if stat, err := os.Stat(absPath + suffix); !os.IsNotExist(err) && !stat.IsDir() {
|
||||
log.Debug("Filepath found via suffix %s: %s", suffix, absPath+suffix)
|
||||
http.ServeFile(w, r, absPath+suffix)
|
||||
return
|
||||
test := absPath + suffix
|
||||
if stat, err := os.Stat(test); !os.IsNotExist(err) && !stat.IsDir() {
|
||||
log.Debug("Filepath found via suffix %s: %s", suffix, test)
|
||||
return Filepath{path + suffix, relPath + suffix, test}, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No file, must be a 404.
|
||||
http.NotFound(w, r)
|
||||
return Filepath{}, errors.New("not found")
|
||||
}
|
||||
|
|
|
@ -1,9 +1,36 @@
|
|||
package core
|
||||
|
||||
import "net/http"
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Redirect sends an HTTP redirect response.
|
||||
func Redirect(w http.ResponseWriter, location string) {
|
||||
func (b *Blog) Redirect(w http.ResponseWriter, location string) {
|
||||
w.Header().Set("Location", location)
|
||||
w.WriteHeader(http.StatusFound)
|
||||
}
|
||||
|
||||
// NotFound sends a 404 response.
|
||||
func (b *Blog) NotFound(w http.ResponseWriter, r *http.Request, message ...string) {
|
||||
if len(message) == 0 {
|
||||
message = []string{"The page you were looking for was not found."}
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
err := b.RenderTemplate(w, r, ".errors/404", map[string]interface{}{
|
||||
"message": message[0],
|
||||
})
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// Forbidden sends an HTTP 400 Forbidden response.
|
||||
func (b *Blog) Forbidden(w http.ResponseWriter, r *http.Request, message ...string) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
err := b.RenderTemplate(w, r, ".errors/403", nil)
|
||||
if err != nil {
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}
|
||||
|
|
61
core/templates.go
Normal file
61
core/templates.go
Normal file
|
@ -0,0 +1,61 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// DefaultVars combines template variables with default, globally available vars.
|
||||
func (b *Blog) DefaultVars(vars map[string]interface{}) map[string]interface{} {
|
||||
defaults := map[string]interface{}{
|
||||
"title": "Untitled Blog",
|
||||
}
|
||||
if vars == nil {
|
||||
return defaults
|
||||
}
|
||||
|
||||
for k, v := range defaults {
|
||||
vars[k] = v
|
||||
}
|
||||
|
||||
return vars
|
||||
}
|
||||
|
||||
// RenderTemplate responds with an HTML template.
|
||||
func (b *Blog) RenderTemplate(w http.ResponseWriter, r *http.Request, path string, vars map[string]interface{}) error {
|
||||
// Get the layout template.
|
||||
layout, err := b.ResolvePath(".layout")
|
||||
if err != nil {
|
||||
log.Error("RenderTemplate(%s): layout template not found", path)
|
||||
return err
|
||||
}
|
||||
|
||||
// And the template in question.
|
||||
filepath, err := b.ResolvePath(path)
|
||||
if err != nil {
|
||||
log.Error("RenderTemplate(%s): file not found", path)
|
||||
return err
|
||||
}
|
||||
|
||||
// Parse the template files. The layout comes first because it's the wrapper
|
||||
// and allows the filepath template to set the page title.
|
||||
t, err := template.ParseFiles(layout.Absolute, filepath.Absolute)
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
// Inject globally available variables.
|
||||
vars = b.DefaultVars(vars)
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; encoding=UTF-8")
|
||||
err = t.ExecuteTemplate(w, "layout", vars)
|
||||
if err != nil {
|
||||
log.Error("Template parsing error: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debug("Parsed template")
|
||||
|
||||
return nil
|
||||
}
|
1
main.go
1
main.go
|
@ -34,5 +34,6 @@ func main() {
|
|||
flag.Parse()
|
||||
|
||||
app := core.New(DocumentRoot, "")
|
||||
app.Debug = fDebug
|
||||
app.ListenAndServe(fAddress)
|
||||
}
|
||||
|
|
3
root/.errors/403.gohtml
Normal file
3
root/.errors/403.gohtml
Normal file
|
@ -0,0 +1,3 @@
|
|||
{{ define "content" }}
|
||||
<h1>403 Forbidden</h1>
|
||||
{{ end }}
|
6
root/.errors/404.gohtml
Normal file
6
root/.errors/404.gohtml
Normal file
|
@ -0,0 +1,6 @@
|
|||
{{ define "title" }}Not Found{{ end }}
|
||||
{{ define "content" }}
|
||||
<h1>404 Not Found</h1>
|
||||
|
||||
{{ .message }}
|
||||
{{ end }}
|
132
root/.layout.gohtml
Normal file
132
root/.layout.gohtml
Normal file
|
@ -0,0 +1,132 @@
|
|||
{{ define "title" }}Untitled{{ end }}
|
||||
{{ define "layout" }}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
|
||||
<title>{{ template "title" or "Untitled" }} - {{ .title }}</title>
|
||||
|
||||
<!-- Bootstrap core CSS -->
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/bluez/theme.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="navbar navbar-expand-md fixed-top bluez-navbar">
|
||||
<a href="#" class="navbar-brand">{{ .title }}</a>
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarCollapse">
|
||||
<ul class="navbar-nav mr-auto">
|
||||
<li class="nav-item active">
|
||||
<a href="/" class="nav-link">Home <span class="sr-only">(current)</span></a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/about" class="nav-link">About</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/archive" class="nav-link">Archive</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<form class="form-inline mt-2 mt-md-0">
|
||||
<input class="form-control mr-sm-2" type="text" placeholder="Search" aria-label="Search">
|
||||
<button class="btn btn-outline-light my-2 my-sm-0" type="submit">Search</button>
|
||||
</form>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="bluez-header">
|
||||
<div class="container">
|
||||
<h1 class="bluez-title">{{ .title }}</h1>
|
||||
<p class="lead bluez-description">Just another web blog.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container mb-5">
|
||||
<div class="row">
|
||||
<div class="col-9">
|
||||
{{ template "content" . }}
|
||||
</div>
|
||||
<div class="col-3">
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<h4 class="card-title">About</h4>
|
||||
|
||||
<p>Hello, world!</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<h4 class="card-title">Archives</h4>
|
||||
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#">March 2018</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#">February 2018</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#">January 2018</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h4 class="card-title">Elsewhere</h4>
|
||||
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#">Facebook</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#">Twitter</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="bluez-footer">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-8">
|
||||
<ul class="nav">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#">RSS</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#">Random</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#">Archive</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#">Ask me anything</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#">Back to top</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
Copyright 2017
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
{{ end }}
|
39
root/admin/setup.gohtml
Normal file
39
root/admin/setup.gohtml
Normal file
|
@ -0,0 +1,39 @@
|
|||
{{ define "title" }}Initial Setup{{ end }}
|
||||
{{ define "content" }}
|
||||
<h1>Initial Setup</h1>
|
||||
|
||||
<p>
|
||||
Welcome to your new web blog! To get started, you'll need to create a username
|
||||
and password to be your <strong>admin user</strong>. You can create additional
|
||||
users for your blog in a later step.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
It is not recommended to name this user "admin" because that would be very
|
||||
predictable for an attacker to guess.
|
||||
</p>
|
||||
|
||||
<form method="POST" action="/admin/setup">
|
||||
<div class="form-group">
|
||||
<label for="setup-admin-username">Admin username:</label>
|
||||
<input type="text" class="form-control" id="setup-admin-username" placeholder="Enter username">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="setup-admin-password1">Passphrase:</label>
|
||||
<input type="password"
|
||||
class="form-control"
|
||||
id="setup-admin-password1"
|
||||
placeholder="correct horse battery staple"
|
||||
aria-describedby="setup-password-help">
|
||||
<small id="setup-password-help" class="form-text text-muted">
|
||||
Choose an <a href="https://xkcd.com/936/" target="_blank">appropriately strong</a> password.
|
||||
</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="setup-admin-password2">Confirm:</label>
|
||||
<input type="password" class="form-control" id="setup-admin-password2" placeholder="correct horse battery staple">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Continue</button>
|
||||
</form>
|
||||
{{ end }}
|
76
root/bluez/theme.css
Normal file
76
root/bluez/theme.css
Normal file
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* Globals
|
||||
*/
|
||||
|
||||
* {
|
||||
transition-duration: 0.5s;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Verdana, Helvetica, Arial, sans-serif;
|
||||
color: #555;
|
||||
margin-top: 80px;
|
||||
background-color: #DEF;
|
||||
}
|
||||
|
||||
h1, .h1,
|
||||
h2, .h2,
|
||||
h3, .h3,
|
||||
h4, .h4,
|
||||
h5, .h5,
|
||||
h6, .h6 {
|
||||
font-family: "Trebuchet MS", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
font-weight: normal;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/*
|
||||
* Bootstrap tweaks and overrides
|
||||
*/
|
||||
.form-group label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/*
|
||||
* Top nav
|
||||
*/
|
||||
.bluez-navbar {
|
||||
background-color: rgba(0, 75, 153, 0.8);
|
||||
}
|
||||
.bluez-navbar a {
|
||||
color: #DDD;
|
||||
}
|
||||
.bluez-navbar a:hover, .bluez-navbar a:active {
|
||||
color: #FFF !important;
|
||||
}
|
||||
.bluez-navbar .active a {
|
||||
color: #FFF;
|
||||
}
|
||||
|
||||
/* Blog title and description */
|
||||
.bluez-header {
|
||||
padding-bottom: 1.25rem;
|
||||
margin-bottom: 2rem;
|
||||
border-bottom: .05rem solid #CCC;
|
||||
}
|
||||
.bluez-title {
|
||||
margin-bottom: 0;
|
||||
font-size: 2rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
.bluez-description {
|
||||
font-size: 1.1rem;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* Page footer */
|
||||
.bluez-footer {
|
||||
padding: 2.5rem 0;
|
||||
color: #999;
|
||||
text-align: center;
|
||||
background-color: #EEF;
|
||||
border-top: .05rem solid #CCC;
|
||||
}
|
||||
.bluez-footer p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
4
root/index.gohtml
Normal file
4
root/index.gohtml
Normal file
|
@ -0,0 +1,4 @@
|
|||
{{ define "title" }}Welcome{{ end }}
|
||||
{{ define "content" }}
|
||||
<h1>Index</h1>
|
||||
{{ end }}
|
|
@ -1,11 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Blog</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>Index</h1>
|
||||
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in New Issue
Block a user