diff --git a/admin.go b/admin.go
index 5f7d0c7..3b05bd4 100644
--- a/admin.go
+++ b/admin.go
@@ -1,501 +1,501 @@
 /*
  * Copyright © 2018-2019 A Bunch Tell LLC.
  *
  * This file is part of WriteFreely.
  *
  * WriteFreely is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License, included
  * in the LICENSE file in this source code package.
  */
 
 package writefreely
 
 import (
 	"database/sql"
 	"fmt"
 	"net/http"
 	"runtime"
 	"strconv"
 	"strings"
 	"time"
 
 	"github.com/gorilla/mux"
 	"github.com/writeas/impart"
 	"github.com/writeas/web-core/auth"
 	"github.com/writeas/web-core/log"
 	"github.com/writeas/web-core/passgen"
 	"github.com/writeas/writefreely/appstats"
 	"github.com/writeas/writefreely/config"
 )
 
 var (
 	appStartTime = time.Now()
 	sysStatus    systemStatus
 )
 
 const adminUsersPerPage = 30
 
 type systemStatus struct {
 	Uptime       string
 	NumGoroutine int
 
 	// General statistics.
 	MemAllocated string // bytes allocated and still in use
 	MemTotal     string // bytes allocated (even if freed)
 	MemSys       string // bytes obtained from system (sum of XxxSys below)
 	Lookups      uint64 // number of pointer lookups
 	MemMallocs   uint64 // number of mallocs
 	MemFrees     uint64 // number of frees
 
 	// Main allocation heap statistics.
 	HeapAlloc    string // bytes allocated and still in use
 	HeapSys      string // bytes obtained from system
 	HeapIdle     string // bytes in idle spans
 	HeapInuse    string // bytes in non-idle span
 	HeapReleased string // bytes released to the OS
 	HeapObjects  uint64 // total number of allocated objects
 
 	// Low-level fixed-size structure allocator statistics.
 	//	Inuse is bytes used now.
 	//	Sys is bytes obtained from system.
 	StackInuse  string // bootstrap stacks
 	StackSys    string
 	MSpanInuse  string // mspan structures
 	MSpanSys    string
 	MCacheInuse string // mcache structures
 	MCacheSys   string
 	BuckHashSys string // profiling bucket hash table
 	GCSys       string // GC metadata
 	OtherSys    string // other system allocations
 
 	// Garbage collector statistics.
 	NextGC       string // next run in HeapAlloc time (bytes)
 	LastGC       string // last run in absolute time (ns)
 	PauseTotalNs string
 	PauseNs      string // circular buffer of recent GC pause times, most recent at [(NumGC+255)%256]
 	NumGC        uint32
 }
 
 type inspectedCollection struct {
 	CollectionObj
 	Followers int
 	LastPost  string
 }
 
 type instanceContent struct {
 	ID      string
 	Type    string
 	Title   sql.NullString
 	Content string
 	Updated time.Time
 }
 
 func (c instanceContent) UpdatedFriendly() string {
 	/*
 		// TODO: accept a locale in this method and use that for the format
 		var loc monday.Locale = monday.LocaleEnUS
 		return monday.Format(u.Created, monday.DateTimeFormatsByLocale[loc], loc)
 	*/
 	return c.Updated.Format("January 2, 2006, 3:04 PM")
 }
 
 func handleViewAdminDash(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
 	updateAppStats()
 	p := struct {
 		*UserPage
 		SysStatus systemStatus
 		Config    config.AppCfg
 
 		Message, ConfigMessage string
 	}{
 		UserPage:  NewUserPage(app, r, u, "Admin", nil),
 		SysStatus: sysStatus,
 		Config:    app.cfg.App,
 
 		Message:       r.FormValue("m"),
 		ConfigMessage: r.FormValue("cm"),
 	}
 
 	showUserPage(w, "admin", p)
 	return nil
 }
 
 func handleViewAdminUsers(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
 	p := struct {
 		*UserPage
 		Config  config.AppCfg
 		Message string
 
 		Users      *[]User
 		CurPage    int
 		TotalUsers int64
 		TotalPages []int
 	}{
 		UserPage: NewUserPage(app, r, u, "Users", nil),
 		Config:   app.cfg.App,
 		Message:  r.FormValue("m"),
 	}
 
 	p.TotalUsers = app.db.GetAllUsersCount()
 	ttlPages := p.TotalUsers / adminUsersPerPage
 	p.TotalPages = []int{}
 	for i := 1; i <= int(ttlPages); i++ {
 		p.TotalPages = append(p.TotalPages, i)
 	}
 
 	var err error
 	p.CurPage, err = strconv.Atoi(r.FormValue("p"))
 	if err != nil || p.CurPage < 1 {
 		p.CurPage = 1
 	} else if p.CurPage > int(ttlPages) {
 		p.CurPage = int(ttlPages)
 	}
 
 	p.Users, err = app.db.GetAllUsers(uint(p.CurPage))
 	if err != nil {
 		return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get users: %v", err)}
 	}
 
 	showUserPage(w, "users", p)
 	return nil
 }
 
 func handleViewAdminUser(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
 	vars := mux.Vars(r)
 	username := vars["username"]
 	if username == "" {
 		return impart.HTTPError{http.StatusFound, "/admin/users"}
 	}
 
 	p := struct {
 		*UserPage
 		Config  config.AppCfg
 		Message string
 
 		User        *User
 		Colls       []inspectedCollection
 		LastPost    string
 		NewPassword string
 		TotalPosts  int64
 		ClearEmail  string
 	}{
 		Config:  app.cfg.App,
 		Message: r.FormValue("m"),
 		Colls:   []inspectedCollection{},
 	}
 
+	var err error
+	p.User, err = app.db.GetUserForAuth(username)
+	if err != nil {
+		return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get user: %v", err)}
+	}
+
 	flashes, _ := getSessionFlashes(app, w, r, nil)
 	for _, flash := range flashes {
 		if strings.HasPrefix(flash, "SUCCESS: ") {
 			p.NewPassword = strings.TrimPrefix(flash, "SUCCESS: ")
-			p.ClearEmail = u.EmailClear(app.keys)
+			p.ClearEmail = p.User.EmailClear(app.keys)
 		}
 	}
-
-	var err error
-	p.User, err = app.db.GetUserForAuth(username)
-	if err != nil {
-		return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get user: %v", err)}
-	}
 	p.UserPage = NewUserPage(app, r, u, p.User.Username, nil)
 	p.TotalPosts = app.db.GetUserPostsCount(p.User.ID)
 	lp, err := app.db.GetUserLastPostTime(p.User.ID)
 	if err != nil {
 		return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get user's last post time: %v", err)}
 	}
 	if lp != nil {
 		p.LastPost = lp.Format("January 2, 2006, 3:04 PM")
 	}
 
 	colls, err := app.db.GetCollections(p.User, app.cfg.App.Host)
 	if err != nil {
 		return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get user's collections: %v", err)}
 	}
 	for _, c := range *colls {
 		ic := inspectedCollection{
 			CollectionObj: CollectionObj{Collection: c},
 		}
 
 		if app.cfg.App.Federation {
 			folls, err := app.db.GetAPFollowers(&c)
 			if err == nil {
 				// TODO: handle error here (at least log it)
 				ic.Followers = len(*folls)
 			}
 		}
 
 		app.db.GetPostsCount(&ic.CollectionObj, true)
 
 		lp, err := app.db.GetCollectionLastPostTime(c.ID)
 		if err != nil {
 			log.Error("Didn't get last post time for collection %d: %v", c.ID, err)
 		}
 		if lp != nil {
 			ic.LastPost = lp.Format("January 2, 2006, 3:04 PM")
 		}
 
 		p.Colls = append(p.Colls, ic)
 	}
 
 	showUserPage(w, "view-user", p)
 	return nil
 }
 
 func handleAdminResetUserPass(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
 	vars := mux.Vars(r)
 	username := vars["username"]
 	if username == "" {
 		return impart.HTTPError{http.StatusFound, "/admin/users"}
 	}
 	// Generate new random password since none supplied
 	pass := passgen.NewWordish()
 	hashedPass, err := auth.HashPass([]byte(pass))
 	if err != nil {
 		return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not create password hash: %v", err)}
 	}
 
 	userIDVal := r.FormValue("user")
 	log.Info("ADMIN: Changing user %s password", userIDVal)
 	id, err := strconv.Atoi(userIDVal)
 	if err != nil {
 		return impart.HTTPError{http.StatusBadRequest, fmt.Sprintf("Invalid user ID: %v", err)}
 	}
 
 	err = app.db.ChangePassphrase(int64(id), true, "", hashedPass)
 	if err != nil {
 		return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not update passphrase: %v", err)}
 	}
 	log.Info("ADMIN: Successfully changed.")
 
 	addSessionFlash(app, w, r, fmt.Sprintf("SUCCESS: %s", pass), nil)
 
 	return impart.HTTPError{http.StatusFound, fmt.Sprintf("/admin/user/%s", username)}
 }
 
 func handleViewAdminPages(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
 	p := struct {
 		*UserPage
 		Config  config.AppCfg
 		Message string
 
 		Pages []*instanceContent
 	}{
 		UserPage: NewUserPage(app, r, u, "Pages", nil),
 		Config:   app.cfg.App,
 		Message:  r.FormValue("m"),
 	}
 
 	var err error
 	p.Pages, err = app.db.GetInstancePages()
 	if err != nil {
 		return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get pages: %v", err)}
 	}
 
 	// Add in default pages
 	var hasAbout, hasPrivacy bool
 	for i, c := range p.Pages {
 		if hasAbout && hasPrivacy {
 			break
 		}
 		if c.ID == "about" {
 			hasAbout = true
 			if !c.Title.Valid {
 				p.Pages[i].Title = defaultAboutTitle(app.cfg)
 			}
 		} else if c.ID == "privacy" {
 			hasPrivacy = true
 			if !c.Title.Valid {
 				p.Pages[i].Title = defaultPrivacyTitle()
 			}
 		}
 	}
 	if !hasAbout {
 		p.Pages = append(p.Pages, &instanceContent{
 			ID:      "about",
 			Title:   defaultAboutTitle(app.cfg),
 			Content: defaultAboutPage(app.cfg),
 			Updated: defaultPageUpdatedTime,
 		})
 	}
 	if !hasPrivacy {
 		p.Pages = append(p.Pages, &instanceContent{
 			ID:      "privacy",
 			Title:   defaultPrivacyTitle(),
 			Content: defaultPrivacyPolicy(app.cfg),
 			Updated: defaultPageUpdatedTime,
 		})
 	}
 
 	showUserPage(w, "pages", p)
 	return nil
 }
 
 func handleViewAdminPage(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
 	vars := mux.Vars(r)
 	slug := vars["slug"]
 	if slug == "" {
 		return impart.HTTPError{http.StatusFound, "/admin/pages"}
 	}
 
 	p := struct {
 		*UserPage
 		Config  config.AppCfg
 		Message string
 
 		Banner  *instanceContent
 		Content *instanceContent
 	}{
 		Config:  app.cfg.App,
 		Message: r.FormValue("m"),
 	}
 
 	var err error
 	// Get pre-defined pages, or select slug
 	if slug == "about" {
 		p.Content, err = getAboutPage(app)
 	} else if slug == "privacy" {
 		p.Content, err = getPrivacyPage(app)
 	} else if slug == "landing" {
 		p.Banner, err = getLandingBanner(app)
 		if err != nil {
 			return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get banner: %v", err)}
 		}
 		p.Content, err = getLandingBody(app)
 		p.Content.ID = "landing"
 	} else if slug == "reader" {
 		p.Content, err = getReaderSection(app)
 	} else {
 		p.Content, err = app.db.GetDynamicContent(slug)
 	}
 	if err != nil {
 		return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get page: %v", err)}
 	}
 	title := "New page"
 	if p.Content != nil {
 		title = "Edit " + p.Content.ID
 	} else {
 		p.Content = &instanceContent{}
 	}
 	p.UserPage = NewUserPage(app, r, u, title, nil)
 
 	showUserPage(w, "view-page", p)
 	return nil
 }
 
 func handleAdminUpdateSite(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
 	vars := mux.Vars(r)
 	id := vars["page"]
 
 	// Validate
 	if id != "about" && id != "privacy" && id != "landing" && id != "reader" {
 		return impart.HTTPError{http.StatusNotFound, "No such page."}
 	}
 
 	var err error
 	m := ""
 	if id == "landing" {
 		// Handle special landing page
 		err = app.db.UpdateDynamicContent("landing-banner", "", r.FormValue("banner"), "section")
 		if err != nil {
 			m = "?m=" + err.Error()
 			return impart.HTTPError{http.StatusFound, "/admin/page/" + id + m}
 		}
 		err = app.db.UpdateDynamicContent("landing-body", "", r.FormValue("content"), "section")
 	} else if id == "reader" {
 		// Update sections with titles
 		err = app.db.UpdateDynamicContent(id, r.FormValue("title"), r.FormValue("content"), "section")
 	} else {
 		// Update page
 		err = app.db.UpdateDynamicContent(id, r.FormValue("title"), r.FormValue("content"), "page")
 	}
 	if err != nil {
 		m = "?m=" + err.Error()
 	}
 	return impart.HTTPError{http.StatusFound, "/admin/page/" + id + m}
 }
 
 func handleAdminUpdateConfig(apper Apper, u *User, w http.ResponseWriter, r *http.Request) error {
 	apper.App().cfg.App.SiteName = r.FormValue("site_name")
 	apper.App().cfg.App.SiteDesc = r.FormValue("site_desc")
 	apper.App().cfg.App.Landing = r.FormValue("landing")
 	apper.App().cfg.App.OpenRegistration = r.FormValue("open_registration") == "on"
 	mul, err := strconv.Atoi(r.FormValue("min_username_len"))
 	if err == nil {
 		apper.App().cfg.App.MinUsernameLen = mul
 	}
 	mb, err := strconv.Atoi(r.FormValue("max_blogs"))
 	if err == nil {
 		apper.App().cfg.App.MaxBlogs = mb
 	}
 	apper.App().cfg.App.Federation = r.FormValue("federation") == "on"
 	apper.App().cfg.App.PublicStats = r.FormValue("public_stats") == "on"
 	apper.App().cfg.App.Private = r.FormValue("private") == "on"
 	apper.App().cfg.App.LocalTimeline = r.FormValue("local_timeline") == "on"
 	if apper.App().cfg.App.LocalTimeline && apper.App().timeline == nil {
 		log.Info("Initializing local timeline...")
 		initLocalTimeline(apper.App())
 	}
 	apper.App().cfg.App.UserInvites = r.FormValue("user_invites")
 	if apper.App().cfg.App.UserInvites == "none" {
 		apper.App().cfg.App.UserInvites = ""
 	}
 	apper.App().cfg.App.DefaultVisibility = r.FormValue("default_visibility")
 
 	m := "?cm=Configuration+saved."
 	err = apper.SaveConfig(apper.App().cfg)
 	if err != nil {
 		m = "?cm=" + err.Error()
 	}
 	return impart.HTTPError{http.StatusFound, "/admin" + m + "#config"}
 }
 
 func updateAppStats() {
 	sysStatus.Uptime = appstats.TimeSincePro(appStartTime)
 
 	m := new(runtime.MemStats)
 	runtime.ReadMemStats(m)
 	sysStatus.NumGoroutine = runtime.NumGoroutine()
 
 	sysStatus.MemAllocated = appstats.FileSize(int64(m.Alloc))
 	sysStatus.MemTotal = appstats.FileSize(int64(m.TotalAlloc))
 	sysStatus.MemSys = appstats.FileSize(int64(m.Sys))
 	sysStatus.Lookups = m.Lookups
 	sysStatus.MemMallocs = m.Mallocs
 	sysStatus.MemFrees = m.Frees
 
 	sysStatus.HeapAlloc = appstats.FileSize(int64(m.HeapAlloc))
 	sysStatus.HeapSys = appstats.FileSize(int64(m.HeapSys))
 	sysStatus.HeapIdle = appstats.FileSize(int64(m.HeapIdle))
 	sysStatus.HeapInuse = appstats.FileSize(int64(m.HeapInuse))
 	sysStatus.HeapReleased = appstats.FileSize(int64(m.HeapReleased))
 	sysStatus.HeapObjects = m.HeapObjects
 
 	sysStatus.StackInuse = appstats.FileSize(int64(m.StackInuse))
 	sysStatus.StackSys = appstats.FileSize(int64(m.StackSys))
 	sysStatus.MSpanInuse = appstats.FileSize(int64(m.MSpanInuse))
 	sysStatus.MSpanSys = appstats.FileSize(int64(m.MSpanSys))
 	sysStatus.MCacheInuse = appstats.FileSize(int64(m.MCacheInuse))
 	sysStatus.MCacheSys = appstats.FileSize(int64(m.MCacheSys))
 	sysStatus.BuckHashSys = appstats.FileSize(int64(m.BuckHashSys))
 	sysStatus.GCSys = appstats.FileSize(int64(m.GCSys))
 	sysStatus.OtherSys = appstats.FileSize(int64(m.OtherSys))
 
 	sysStatus.NextGC = appstats.FileSize(int64(m.NextGC))
 	sysStatus.LastGC = fmt.Sprintf("%.1fs", float64(time.Now().UnixNano()-int64(m.LastGC))/1000/1000/1000)
 	sysStatus.PauseTotalNs = fmt.Sprintf("%.1fs", float64(m.PauseTotalNs)/1000/1000/1000)
 	sysStatus.PauseNs = fmt.Sprintf("%.3fs", float64(m.PauseNs[(m.NumGC+255)%256])/1000/1000/1000)
 	sysStatus.NumGC = m.NumGC
 }
 
 func adminResetPassword(app *App, u *User, newPass string) error {
 	hashedPass, err := auth.HashPass([]byte(newPass))
 	if err != nil {
 		return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not create password hash: %v", err)}
 	}
 
 	err = app.db.ChangePassphrase(u.ID, true, "", hashedPass)
 	if err != nil {
 		return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not update passphrase: %v", err)}
 	}
 	return nil
 }
diff --git a/templates/user/admin/view-user.tmpl b/templates/user/admin/view-user.tmpl
index 720a714..4157fca 100644
--- a/templates/user/admin/view-user.tmpl
+++ b/templates/user/admin/view-user.tmpl
@@ -1,123 +1,123 @@
 {{define "view-user"}}
 {{template "header" .}}
 <style>
 table.classy th {
 	text-align: left;
 }
 h3 {
 	font-weight: normal;
 }
 input.copy-text {
 	text-align: center;
 	font-size: 1.2em;
 	color: #555;
 	width: 100%;
 	box-sizing: border-box;
 }
 </style>
 <div class="snug content-container">
 	{{template "admin-header" .}}
 
 	<h2 id="posts-header">{{.User.Username}}</h2>
 	{{if .NewPassword}}<div class="alert success">
 		<p>This user's password has been reset to:</p>
 		<p><input type="text" class="copy-text" value="{{.NewPassword}}" onfocus="if (this.select) this.select(); else this.setSelectionRange(0, this.value.length);" readonly /></p>
 		<p>They can use this new password to log in to their account. <strong>This will only be shown once</strong>, so be sure to copy it and send it to them now.</p>
-		{{if .User.Email}}<p>Their email address is: <a href="mailto:{{.ClearEmail}}">{{.ClearEmail}}</a></p>{{end}}
+		{{if .ClearEmail}}<p>Their email address is: <a href="mailto:{{.ClearEmail}}">{{.ClearEmail}}</a></p>{{end}}
 		</div>
 	{{end}}
 	<table class="classy export">
 		<tr>
 			<th>No.</th>
 			<td>{{.User.ID}}</td>
 		</tr>
 		<tr>
 			<th>Type</th>
 			<td>{{if .User.IsAdmin}}Admin{{else}}User{{end}}</td>
 		</tr>
 		<tr>
 			<th>Username</th>
 			<td>{{.User.Username}}</td>
 		</tr>
 		<tr>
 			<th>Joined</th>
 			<td>{{.User.CreatedFriendly}}</td>
 		</tr>
 		<tr>
 			<th>Total Posts</th>
 			<td>{{.TotalPosts}}</td>
 		</tr>
 		<tr>
 			<th>Last Post</th>
 			<td>{{if .LastPost}}{{.LastPost}}{{else}}Never{{end}}</td>
 		</tr>
 		<tr>
 			<th>Password</th>
 			<td>
 				{{if ne .Username .User.Username}}
 				<form id="reset-form" action="/admin/user/{{.User.Username}}/passphrase" method="post" autocomplete="false">
 					<input type="hidden" name="user" value="{{.User.ID}}"/>
 					<button type="submit">Reset</button>
 				</form>
 				{{else}}
 				<a href="/me/settings" title="Go to reset password page">Change your password</a>
 				{{end}}
 			</td>
 		</tr>
 	</table>
 
 	<h2>Blogs</h2>
 
 	{{range .Colls}}
 	<h3><a href="/{{.Alias}}/">{{.Title}}</a></h3>
 	<table class="classy export">
 		<tr>
 			<th>Alias</th>
 			<td>{{.Alias}}</td>
 		</tr>
 		<tr>
 			<th>Title</th>
 			<td>{{.Title}}</td>
 		</tr>
 		<tr>
 			<th>Description</th>
 			<td>{{.Description}}</td>
 		</tr>
 		<tr>
 			<th>Visibility</th>
 			<td>{{.FriendlyVisibility}}</td>
 		</tr>
 		<tr>
 			<th>Views</th>
 			<td>{{.Views}}</td>
 		</tr>
 		<tr>
 			<th>Posts</th>
 			<td>{{.TotalPosts}}</td>
 		</tr>
 		<tr>
 			<th>Last Post</th>
 			<td>{{if .LastPost}}{{.LastPost}}{{else}}Never{{end}}</td>
 		</tr>
 		{{if $.Config.Federation}}
 		<tr>
 			<th>Fediverse Followers</th>
 			<td>{{.Followers}}</td>
 		</tr>
 		{{end}}
 	</table>
 	{{end}}
 </div>
 
 <script type="text/javascript">
 	form = document.getElementById("reset-form");
 	form.addEventListener('submit', function(e) {
 		e.preventDefault();
 		agreed = confirm("Reset this user's password? This will generate a new temporary password that you'll need to share with them, and invalidate their old one.");
 		if (agreed === true) {
 			form.submit();
 		}
 	});
 </script>
 {{template "footer" .}}
 {{end}}