Initial "Bluez" default theme and Admin Setup page

This commit is contained in:
Noah 2017-10-31 09:42:15 -07:00
parent 0c243c849c
commit 456cad7a50
14 changed files with 426 additions and 22 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
bin/ bin/
dist/ dist/
root/.private

10
core/admin.go Normal file
View 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)
}

View File

@ -10,6 +10,8 @@ import (
// Blog is the root application object that maintains the app configuration // Blog is the root application object that maintains the app configuration
// and helper objects. // and helper objects.
type Blog struct { type Blog struct {
Debug bool
// DocumentRoot is the core static files root; UserRoot masks over it. // DocumentRoot is the core static files root; UserRoot masks over it.
DocumentRoot string DocumentRoot string
UserRoot string UserRoot string
@ -27,6 +29,7 @@ func New(documentRoot, userRoot string) *Blog {
} }
r := mux.NewRouter() r := mux.NewRouter()
blog.r = r blog.r = r
r.HandleFunc("/admin/setup", blog.SetupHandler)
r.HandleFunc("/", blog.PageHandler) r.HandleFunc("/", blog.PageHandler)
r.NotFoundHandler = http.HandlerFunc(blog.PageHandler) r.NotFoundHandler = http.HandlerFunc(blog.PageHandler)

View File

@ -1,6 +1,7 @@
package core package core
import ( import (
"errors"
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
@ -13,13 +14,64 @@ func (b *Blog) PageHandler(w http.ResponseWriter, r *http.Request) {
// Remove trailing slashes by redirecting them away. // Remove trailing slashes by redirecting them away.
if len(path) > 1 && path[len(path)-1] == '/' { 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 return
} }
// Search for a file that matches their URL. // 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) log.Debug("Resolving filepath for URI: %s", path)
for _, root := range []string{b.DocumentRoot, b.UserRoot} { for _, root := range []string{b.DocumentRoot, b.UserRoot} {
if len(root) == 0 {
continue
}
// Resolve the file path.
relPath := filepath.Join(root, path) relPath := filepath.Join(root, path)
absPath, err := filepath.Abs(relPath) absPath, err := filepath.Abs(relPath)
if err != nil { if err != nil {
@ -31,26 +83,26 @@ func (b *Blog) PageHandler(w http.ResponseWriter, r *http.Request) {
// Found an exact hit? // Found an exact hit?
if stat, err := os.Stat(absPath); !os.IsNotExist(err) && !stat.IsDir() { if stat, err := os.Stat(absPath); !os.IsNotExist(err) && !stat.IsDir() {
log.Debug("Exact filepath found: %s", absPath) log.Debug("Exact filepath found: %s", absPath)
http.ServeFile(w, r, absPath) return Filepath{path, relPath, absPath}, nil
return
} }
// Try some supported suffixes. // Try some supported suffixes.
suffixes := []string{ suffixes := []string{
".gohtml",
".html", ".html",
"/index.gohtml",
"/index.html", "/index.html",
".md", ".md",
"/index.md", "/index.md",
} }
for _, suffix := range suffixes { for _, suffix := range suffixes {
if stat, err := os.Stat(absPath + suffix); !os.IsNotExist(err) && !stat.IsDir() { test := absPath + suffix
log.Debug("Filepath found via suffix %s: %s", suffix, absPath+suffix) if stat, err := os.Stat(test); !os.IsNotExist(err) && !stat.IsDir() {
http.ServeFile(w, r, absPath+suffix) log.Debug("Filepath found via suffix %s: %s", suffix, test)
return return Filepath{path + suffix, relPath + suffix, test}, nil
} }
} }
} }
// No file, must be a 404. return Filepath{}, errors.New("not found")
http.NotFound(w, r)
} }

View File

@ -1,9 +1,36 @@
package core package core
import "net/http" import (
"net/http"
)
// Redirect sends an HTTP redirect response. // 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.Header().Set("Location", location)
w.WriteHeader(http.StatusFound) 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
View 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
}

View File

@ -34,5 +34,6 @@ func main() {
flag.Parse() flag.Parse()
app := core.New(DocumentRoot, "") app := core.New(DocumentRoot, "")
app.Debug = fDebug
app.ListenAndServe(fAddress) app.ListenAndServe(fAddress)
} }

3
root/.errors/403.gohtml Normal file
View File

@ -0,0 +1,3 @@
{{ define "content" }}
<h1>403 Forbidden</h1>
{{ end }}

6
root/.errors/404.gohtml Normal file
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,4 @@
{{ define "title" }}Welcome{{ end }}
{{ define "content" }}
<h1>Index</h1>
{{ end }}

View File

@ -1,11 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Blog</title>
</head>
<body>
<h1>Index</h1>
</body>
</html>