Handle drafts and private/unlisted posts

This commit is contained in:
Noah 2017-11-24 12:53:13 -08:00
parent b127c61dd7
commit 34d444ab96
11 changed files with 185 additions and 39 deletions

View File

@ -22,9 +22,16 @@ func (b *Blog) Login(w http.ResponseWriter, r *http.Request, u *users.User) erro
// LoginHandler shows and handles the login page. // LoginHandler shows and handles the login page.
func (b *Blog) LoginHandler(w http.ResponseWriter, r *http.Request) { func (b *Blog) LoginHandler(w http.ResponseWriter, r *http.Request) {
vars := &Vars{ vars := NewVars()
Form: forms.Setup{}, vars.Form = forms.Setup{}
var nextURL string
if r.Method == http.MethodPost {
nextURL = r.FormValue("next")
} else {
nextURL = r.URL.Query().Get("next")
} }
vars.Data["NextURL"] = nextURL
if r.Method == http.MethodPost { if r.Method == http.MethodPost {
form := &forms.Login{ form := &forms.Login{
@ -46,10 +53,9 @@ func (b *Blog) LoginHandler(w http.ResponseWriter, r *http.Request) {
b.Login(w, r, user) b.Login(w, r, user)
// A next URL given? TODO: actually get to work // A next URL given? TODO: actually get to work
next := r.FormValue("next") log.Info("Redirect after login to: %s", nextURL)
log.Info("Redirect after login to: %s", next) if len(nextURL) > 0 && nextURL[0] == '/' {
if len(next) > 0 && next[0] == '/' { b.Redirect(w, nextURL)
b.Redirect(w, next)
} else { } else {
b.Redirect(w, "/") b.Redirect(w, "/")
} }

View File

@ -28,11 +28,14 @@ type PostMeta struct {
func (b *Blog) BlogRoutes(r *mux.Router) { func (b *Blog) BlogRoutes(r *mux.Router) {
// Public routes // Public routes
r.HandleFunc("/blog", b.BlogIndex) r.HandleFunc("/blog", b.BlogIndex)
r.HandleFunc("/tagged/{tag}", b.Tagged)
// Login-required routers. // Login-required routers.
loginRouter := mux.NewRouter() loginRouter := mux.NewRouter()
loginRouter.HandleFunc("/blog/edit", b.EditBlog) loginRouter.HandleFunc("/blog/edit", b.EditBlog)
loginRouter.HandleFunc("/blog/delete", b.DeletePost) loginRouter.HandleFunc("/blog/delete", b.DeletePost)
loginRouter.HandleFunc("/blog/drafts", b.Drafts)
loginRouter.HandleFunc("/blog/private", b.PrivatePosts)
r.PathPrefix("/blog").Handler( r.PathPrefix("/blog").Handler(
negroni.New( negroni.New(
negroni.HandlerFunc(b.LoginRequired), negroni.HandlerFunc(b.LoginRequired),
@ -52,6 +55,33 @@ func (b *Blog) BlogRoutes(r *mux.Router) {
// BlogIndex renders the main index page of the blog. // BlogIndex renders the main index page of the blog.
func (b *Blog) BlogIndex(w http.ResponseWriter, r *http.Request) { func (b *Blog) BlogIndex(w http.ResponseWriter, r *http.Request) {
b.PartialIndex(w, r, "", "")
}
// Tagged lets you browse blog posts by category.
func (b *Blog) Tagged(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
tag, ok := params["tag"]
if !ok {
b.BadRequest(w, r, "Missing category in URL")
}
b.PartialIndex(w, r, tag, "")
}
// Drafts renders an index view of only draft posts. Login required.
func (b *Blog) Drafts(w http.ResponseWriter, r *http.Request) {
b.PartialIndex(w, r, "", DRAFT)
}
// PrivatePosts renders an index view of only private posts. Login required.
func (b *Blog) PrivatePosts(w http.ResponseWriter, r *http.Request) {
b.PartialIndex(w, r, "", PRIVATE)
}
// PartialIndex handles common logic for blog index views.
func (b *Blog) PartialIndex(w http.ResponseWriter, r *http.Request,
tag, privacy string) {
v := NewVars(map[interface{}]interface{}{}) v := NewVars(map[interface{}]interface{}{})
// Get the blog index. // Get the blog index.
@ -60,9 +90,52 @@ func (b *Blog) BlogIndex(w http.ResponseWriter, r *http.Request) {
// The set of blog posts to show. // The set of blog posts to show.
var pool []posts.Post var pool []posts.Post
for _, post := range idx.Posts { for _, post := range idx.Posts {
// Limiting by a specific privacy setting? (drafts or private only)
if privacy != "" {
switch privacy {
case DRAFT:
if post.Privacy != DRAFT {
continue
}
case PRIVATE:
if post.Privacy != PRIVATE && post.Privacy != UNLISTED {
continue
}
}
} else {
// Exclude certain posts in generic index views.
if (post.Privacy == PRIVATE || post.Privacy == UNLISTED) && !b.LoggedIn(r) {
continue
} else if post.Privacy == DRAFT {
continue
}
}
// Limit by tag?
if tag != "" {
var tagMatch bool
if tag != "" {
for _, check := range post.Tags {
if check == tag {
tagMatch = true
break
}
}
}
if !tagMatch {
continue
}
}
pool = append(pool, post) pool = append(pool, post)
} }
if len(pool) == 0 {
b.NotFound(w, r, "No blog posts were found.")
return
}
sort.Sort(sort.Reverse(posts.ByUpdated(pool))) sort.Sort(sort.Reverse(posts.ByUpdated(pool)))
// Query parameters. // Query parameters.
@ -106,7 +179,7 @@ func (b *Blog) BlogIndex(w http.ResponseWriter, r *http.Request) {
} }
// Render the post. // Render the post.
if post.ContentType == "markdown" { if post.ContentType == string(MARKDOWN) {
rendered = template.HTML(b.RenderTrustedMarkdown(post.Body)) rendered = template.HTML(b.RenderTrustedMarkdown(post.Body))
} else { } else {
rendered = template.HTML(post.Body) rendered = template.HTML(post.Body)
@ -138,6 +211,14 @@ func (b *Blog) viewPost(w http.ResponseWriter, r *http.Request, fragment string)
return err return err
} }
// Handle post privacy.
if post.Privacy == PRIVATE || post.Privacy == DRAFT {
if !b.LoggedIn(r) {
b.NotFound(w, r)
return nil
}
}
v := NewVars(map[interface{}]interface{}{ v := NewVars(map[interface{}]interface{}{
"Post": post, "Post": post,
}) })
@ -170,7 +251,7 @@ func (b *Blog) RenderPost(p *posts.Post, indexView bool) template.HTML {
// Render the post to HTML. // Render the post to HTML.
var rendered template.HTML var rendered template.HTML
if p.ContentType == "markdown" { if p.ContentType == string(MARKDOWN) {
rendered = template.HTML(b.RenderTrustedMarkdown(p.Body)) rendered = template.HTML(b.RenderTrustedMarkdown(p.Body))
} else { } else {
rendered = template.HTML(p.Body) rendered = template.HTML(p.Body)
@ -234,7 +315,7 @@ func (b *Blog) EditBlog(w http.ResponseWriter, r *http.Request) {
// Previewing, or submitting? // Previewing, or submitting?
switch r.FormValue("submit") { switch r.FormValue("submit") {
case "preview": case "preview":
if post.ContentType == "markdown" || post.ContentType == "markdown+html" { if post.ContentType == string(MARKDOWN) {
v.Data["preview"] = template.HTML(b.RenderMarkdown(post.Body)) v.Data["preview"] = template.HTML(b.RenderMarkdown(post.Body))
} else { } else {
v.Data["preview"] = template.HTML(post.Body) v.Data["preview"] = template.HTML(post.Body)

21
core/constants.go Normal file
View File

@ -0,0 +1,21 @@
package core
// PostPrivacy values.
type PostPrivacy string
// ContentType values
type ContentType string
// Post privacy constants.
const (
PUBLIC PostPrivacy = "public"
PRIVATE = "private"
UNLISTED = "unlisted"
DRAFT = "draft"
)
// Content types for blog posts.
const (
MARKDOWN ContentType = "markdown"
HTML ContentType = "html"
)

View File

@ -76,7 +76,18 @@ func (b *Blog) CurrentUser(r *http.Request) (*users.User, error) {
return u, err return u, err
} }
return &users.User{}, errors.New("not authenticated") return &users.User{
Admin: false,
}, errors.New("not authenticated")
}
// LoggedIn returns whether the current user is logged in to an account.
func (b *Blog) LoggedIn(r *http.Request) bool {
session := b.Session(r)
if loggedIn, ok := session.Values["logged-in"].(bool); ok && loggedIn {
return true
}
return false
} }
// AuthMiddleware loads the user's authentication state. // AuthMiddleware loads the user's authentication state.

View File

@ -82,7 +82,7 @@ func (p *Post) Validate() error {
p.ContentType != "html" { p.ContentType != "html" {
return errors.New("invalid setting for ContentType") return errors.New("invalid setting for ContentType")
} }
if p.Privacy != "public" && p.Privacy != "draft" && p.Privacy != "private" { if p.Privacy != "public" && p.Privacy != "draft" && p.Privacy != "private" && p.Privacy != "unlisted" {
return errors.New("invalid setting for Privacy") return errors.New("invalid setting for Privacy")
} }
return nil return nil

View File

@ -60,6 +60,10 @@ func (v *Vars) LoadDefaults(b *Blog, w http.ResponseWriter, r *http.Request) {
v.Title = s.Site.Title v.Title = s.Site.Title
v.Path = r.URL.Path v.Path = r.URL.Path
user, err := b.CurrentUser(r)
v.CurrentUser = user
v.LoggedIn = err == nil
// Add any flashed messages from the endpoint controllers. // Add any flashed messages from the endpoint controllers.
session := b.Session(r) session := b.Session(r)
if flashes := session.Flashes(); len(flashes) > 0 { if flashes := session.Flashes(); len(flashes) > 0 {
@ -71,14 +75,6 @@ func (v *Vars) LoadDefaults(b *Blog, w http.ResponseWriter, r *http.Request) {
} }
v.CSRF = b.GenerateCSRFToken(w, r, session) v.CSRF = b.GenerateCSRFToken(w, r, session)
ctx := r.Context()
if user, ok := ctx.Value(userKey).(*users.User); ok {
if user.ID > 0 {
v.LoggedIn = true
v.CurrentUser = user
}
}
} }
// TemplateVars is an interface that describes the template variable struct. // TemplateVars is an interface that describes the template variable struct.

View File

@ -82,20 +82,37 @@
<div class="card-body"> <div class="card-body">
<h4 class="card-title">About</h4> <h4 class="card-title">About</h4>
{{ if .LoggedIn }}
Hello, {{ .CurrentUser.Username }}.<br>
<a href="/logout">Log out</a><br>
{{ if .CurrentUser.Admin }}
<a href="/admin">Admin center</a>
{{ end }}
{{ else }}
<a href="/login">Log in</a>
{{ end }}
<p>Hello, world!</p> <p>Hello, world!</p>
</div> </div>
</div> </div>
{{ if .LoggedIn }}
<div class="card mb-4">
<div class="card-body">
<h4 class="cart-title">Control Center</h4>
<p>
Logged in as: <a href="/u/{{ .CurrentUser.Username }}">{{ .CurrentUser.Username }}</a>
</p>
<ul class="list-unstyled">
{{ if .CurrentUser.Admin }}
<li class="list-item"><a href="/admin">Admin Center</a></li>
{{ end }}
<li class="list-item"><a href="/logout">Log out</a></li>
</ul>
<h5>Manage Blog</h5>
<ul class="list-unstyled">
<li class="list-item"><a href="/blog/edit">Post Blog Entry</a></li>
<li class="list-item"><a href="/blog/drafts">View Drafts</a></li>
<li class="list-item"><a href="/blog/private">View Private</a></li>
</ul>
</div>
</div>
{{ end }}
<div class="card mb-4"> <div class="card mb-4">
<div class="card-body"> <div class="card-body">
<h4 class="card-title">Archives</h4> <h4 class="card-title">Archives</h4>
@ -153,6 +170,15 @@
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="#">Back to top</a> <a class="nav-link" href="#">Back to top</a>
</li> </li>
{{ if .LoggedIn }}
<li class="nav-item">
<a class="nav-link" href="/logout">Log out</a>
</li>
{{ else }}
<li class="nav-item">
<a class="nav-link" href="/login">Log in</a>
</li>
{{ end }}
</ul> </ul>
</div> </div>
<div class="col-4"> <div class="col-4">

View File

@ -56,16 +56,6 @@
> Markdown > Markdown
</label> </label>
</div> </div>
<div class="form-check form-check-inline">
<label class="form-check-label">
<input type="radio"
class="form-check-input"
name="content-type"
value="markdown+html"
{{ if eq .ContentType "markdown+html" }}checked{{ end }}
> Markdown + HTML
</label>
</div>
<div class="form-check form-check-inline"> <div class="form-check form-check-inline">
<label class="form-check-label"> <label class="form-check-label">
<input type="radio" <input type="radio"
@ -99,6 +89,7 @@
<select name="privacy" class="form-control"> <select name="privacy" class="form-control">
<option value="public"{{ if eq .Privacy "public" }} selected{{ end }}>Public: everybody can see this post</option> <option value="public"{{ if eq .Privacy "public" }} selected{{ end }}>Public: everybody can see this post</option>
<option value="private"{{ if eq .Privacy "private" }} selected{{ end }}>Private: only site admins can see this post</option> <option value="private"{{ if eq .Privacy "private" }} selected{{ end }}>Private: only site admins can see this post</option>
<option value="unlisted"{{ if eq .Privacy "unlisted" }} selected{{ end }}>Unlisted: only those with the direct link can see it</option>
<option value="draft"{{ if eq .Privacy "draft" }} selected{{ end }}>Draft: don't show this post on the blog anywhere</option> <option value="draft"{{ if eq .Privacy "draft" }} selected{{ end }}>Draft: don't show this post on the blog anywhere</option>
</select> </select>
</div> </div>

View File

@ -8,6 +8,13 @@
{{ end }} {{ end }}
<div class="blog-meta"> <div class="blog-meta">
{{ if eq $p.Privacy "private" }}
<span class="blog-private">[private]</span>
{{ else if eq $p.Privacy "draft" }}
<span class="blog-draft">[draft]</span>
{{ else if eq $p.Privacy "unlisted" }}
<span class="blog-unlisted">[unlisted]</span>
{{ end }}
<span title="{{ $p.Created.Format "Jan 2 2006 @ 15:04:05 MST" }}"> <span title="{{ $p.Created.Format "Jan 2 2006 @ 15:04:05 MST" }}">
{{ $p.Created.Format "January 2, 2006" }} {{ $p.Created.Format "January 2, 2006" }}
</span> </span>

View File

@ -16,6 +16,12 @@ a.blog-title {
font-size: smaller; font-size: smaller;
margin-bottom: 2rem; margin-bottom: 2rem;
} }
.blog-meta .blog-private, .blog-meta .blog-unlisted {
color: #F00;
}
.blog-meta .blog-draft {
color: #909;
}
/* Code blocks treated as <pre> tags */ /* Code blocks treated as <pre> tags */
.markdown code { .markdown code {

View File

@ -4,6 +4,7 @@
<form name="login" action="/login" method="POST"> <form name="login" action="/login" method="POST">
<input type="hidden" name="_csrf" value="{{ .CSRF }}"> <input type="hidden" name="_csrf" value="{{ .CSRF }}">
<input type="hidden" name="next" value="{{ .Data.NextURL }}">
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<input type="text" name="username" class="form-control" placeholder="Username"> <input type="text" name="username" class="form-control" placeholder="Username">