Page Menu
Home
Musing Studio
Search
Configure Global Search
Log In
Files
F10455838
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
54 KB
Subscribers
None
View Options
diff --git a/admin.go b/admin.go
index 5f7d244..33445ac 100644
--- a/admin.go
+++ b/admin.go
@@ -1,530 +1,575 @@
/*
- * Copyright © 2018-2019 A Bunch Tell LLC.
+ * Copyright © 2018-2020 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 {
+ p := struct {
+ *UserPage
+ Message string
+
+ UsersCount, CollectionsCount, PostsCount int64
+ }{
+ UserPage: NewUserPage(app, r, u, "Admin", nil),
+ Message: r.FormValue("m"),
+ }
+
+ // Get user stats
+ p.UsersCount = app.db.GetAllUsersCount()
+ var err error
+ p.CollectionsCount, err = app.db.GetTotalCollections()
+ if err != nil {
+ return err
+ }
+ p.PostsCount, err = app.db.GetTotalPosts()
+ if err != nil {
+ return err
+ }
+
+ showUserPage(w, "admin", p)
+ return nil
+}
+
+func handleViewAdminMonitor(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)
+ showUserPage(w, "monitor", p)
+ return nil
+}
+
+func handleViewAdminSettings(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
+ p := struct {
+ *UserPage
+ Config config.AppCfg
+
+ Message, ConfigMessage string
+ }{
+ UserPage: NewUserPage(app, r, u, "Admin", nil),
+ Config: app.cfg.App,
+
+ Message: r.FormValue("m"),
+ ConfigMessage: r.FormValue("cm"),
+ }
+
+ showUserPage(w, "app-settings", 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 {
if err == ErrUserNotFound {
return err
}
log.Error("Could not get user: %v", err)
return impart.HTTPError{http.StatusInternalServerError, err.Error()}
}
flashes, _ := getSessionFlashes(app, w, r, nil)
for _, flash := range flashes {
if strings.HasPrefix(flash, "SUCCESS: ") {
p.NewPassword = strings.TrimPrefix(flash, "SUCCESS: ")
p.ClearEmail = p.User.EmailClear(app.keys)
}
}
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 handleAdminToggleUserStatus(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"}
}
user, err := app.db.GetUserForAuth(username)
if err != nil {
log.Error("failed to get user: %v", err)
return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get user from username: %v", err)}
}
if user.IsSilenced() {
err = app.db.SetUserStatus(user.ID, UserActive)
} else {
err = app.db.SetUserStatus(user.ID, UserSilenced)
}
if err != nil {
log.Error("toggle user silenced: %v", err)
return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not toggle user status: %v", err)}
}
return impart.HTTPError{http.StatusFound, fmt.Sprintf("/admin/user/%s#status", username)}
}
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"}
+ return impart.HTTPError{http.StatusFound, "/admin/settings" + 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/less/admin.less b/less/admin.less
index 9c4a7c2..3cbf30b 100644
--- a/less/admin.less
+++ b/less/admin.less
@@ -1,44 +1,44 @@
.edit-page {
font-size: 1em;
min-height: 12em;
}
header.admin {
margin: 0;
h1 + a {
margin-left: 1em;
}
}
nav#admin {
display: block;
margin: 0.5em 0;
a {
- color: @primary;
- &:first-child {
- margin-left: 0;
- }
+ margin-left: 0;
+ .rounded(.25em);
+ border: 0;
&.selected {
+ background: #dedede;
font-weight: bold;
}
}
}
.pager {
display: flex;
justify-content: center;
a {
color: #333;
font-family: @sansFont;
font-size: 0.86em;
padding: 0.5em 1em;
border: 1px solid #ccc;
&:hover {
text-decoration: none;
background: #efefef;
}
&.selected {
cursor: default;
background: #ccc;
}
}
}
diff --git a/routes.go b/routes.go
index fcd00ec..e606fbc 100644
--- a/routes.go
+++ b/routes.go
@@ -1,219 +1,221 @@
/*
* 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 (
"net/http"
"path/filepath"
"strings"
"github.com/gorilla/mux"
"github.com/writeas/go-webfinger"
"github.com/writeas/web-core/log"
"github.com/writefreely/go-nodeinfo"
)
// InitStaticRoutes adds routes for serving static files.
// TODO: this should just be a func, not method
func (app *App) InitStaticRoutes(r *mux.Router) {
// Handle static files
fs := http.FileServer(http.Dir(filepath.Join(app.cfg.Server.StaticParentDir, staticDir)))
app.shttp = http.NewServeMux()
app.shttp.Handle("/", fs)
r.PathPrefix("/").Handler(fs)
}
// InitRoutes adds dynamic routes for the given mux.Router.
func InitRoutes(apper Apper, r *mux.Router) *mux.Router {
// Create handler
handler := NewWFHandler(apper)
// Set up routes
hostSubroute := apper.App().cfg.App.Host[strings.Index(apper.App().cfg.App.Host, "://")+3:]
if apper.App().cfg.App.SingleUser {
hostSubroute = "{domain}"
} else {
if strings.HasPrefix(hostSubroute, "localhost") {
hostSubroute = "localhost"
}
}
if apper.App().cfg.App.SingleUser {
log.Info("Adding %s routes (single user)...", hostSubroute)
} else {
log.Info("Adding %s routes (multi-user)...", hostSubroute)
}
// Primary app routes
write := r.PathPrefix("/").Subrouter()
// Federation endpoint configurations
wf := webfinger.Default(wfResolver{apper.App().db, apper.App().cfg})
wf.NoTLSHandler = nil
// Federation endpoints
// host-meta
write.HandleFunc("/.well-known/host-meta", handler.Web(handleViewHostMeta, UserLevelReader))
// webfinger
write.HandleFunc(webfinger.WebFingerPath, handler.LogHandlerFunc(http.HandlerFunc(wf.Webfinger)))
// nodeinfo
niCfg := nodeInfoConfig(apper.App().db, apper.App().cfg)
ni := nodeinfo.NewService(*niCfg, nodeInfoResolver{apper.App().cfg, apper.App().db})
write.HandleFunc(nodeinfo.NodeInfoPath, handler.LogHandlerFunc(http.HandlerFunc(ni.NodeInfoDiscover)))
write.HandleFunc(niCfg.InfoURL, handler.LogHandlerFunc(http.HandlerFunc(ni.NodeInfo)))
// handle mentions
write.HandleFunc("/@/{handle}", handler.Web(handleViewMention, UserLevelReader))
configureSlackOauth(handler, write, apper.App())
configureWriteAsOauth(handler, write, apper.App())
// Set up dyamic page handlers
// Handle auth
auth := write.PathPrefix("/api/auth/").Subrouter()
if apper.App().cfg.App.OpenRegistration {
auth.HandleFunc("/signup", handler.All(apiSignup)).Methods("POST")
}
auth.HandleFunc("/login", handler.All(login)).Methods("POST")
auth.HandleFunc("/read", handler.WebErrors(handleWebCollectionUnlock, UserLevelNone)).Methods("POST")
auth.HandleFunc("/me", handler.All(handleAPILogout)).Methods("DELETE")
// Handle logged in user sections
me := write.PathPrefix("/me").Subrouter()
me.HandleFunc("/", handler.Redirect("/me", UserLevelUser))
me.HandleFunc("/c", handler.Redirect("/me/c/", UserLevelUser)).Methods("GET")
me.HandleFunc("/c/", handler.User(viewCollections)).Methods("GET")
me.HandleFunc("/c/{collection}", handler.User(viewEditCollection)).Methods("GET")
me.HandleFunc("/c/{collection}/stats", handler.User(viewStats)).Methods("GET")
me.HandleFunc("/posts", handler.Redirect("/me/posts/", UserLevelUser)).Methods("GET")
me.HandleFunc("/posts/", handler.User(viewArticles)).Methods("GET")
me.HandleFunc("/posts/export.csv", handler.Download(viewExportPosts, UserLevelUser)).Methods("GET")
me.HandleFunc("/posts/export.zip", handler.Download(viewExportPosts, UserLevelUser)).Methods("GET")
me.HandleFunc("/posts/export.json", handler.Download(viewExportPosts, UserLevelUser)).Methods("GET")
me.HandleFunc("/export", handler.User(viewExportOptions)).Methods("GET")
me.HandleFunc("/export.json", handler.Download(viewExportFull, UserLevelUser)).Methods("GET")
me.HandleFunc("/import", handler.User(viewImport)).Methods("GET")
me.HandleFunc("/settings", handler.User(viewSettings)).Methods("GET")
me.HandleFunc("/invites", handler.User(handleViewUserInvites)).Methods("GET")
me.HandleFunc("/logout", handler.Web(viewLogout, UserLevelNone)).Methods("GET")
write.HandleFunc("/api/me", handler.All(viewMeAPI)).Methods("GET")
apiMe := write.PathPrefix("/api/me/").Subrouter()
apiMe.HandleFunc("/", handler.All(viewMeAPI)).Methods("GET")
apiMe.HandleFunc("/posts", handler.UserAPI(viewMyPostsAPI)).Methods("GET")
apiMe.HandleFunc("/collections", handler.UserAPI(viewMyCollectionsAPI)).Methods("GET")
apiMe.HandleFunc("/password", handler.All(updatePassphrase)).Methods("POST")
apiMe.HandleFunc("/self", handler.All(updateSettings)).Methods("POST")
apiMe.HandleFunc("/invites", handler.User(handleCreateUserInvite)).Methods("POST")
apiMe.HandleFunc("/import", handler.User(handleImport)).Methods("POST")
// Sign up validation
write.HandleFunc("/api/alias", handler.All(handleUsernameCheck)).Methods("POST")
write.HandleFunc("/api/markdown", handler.All(handleRenderMarkdown)).Methods("POST")
// Handle collections
write.HandleFunc("/api/collections", handler.All(newCollection)).Methods("POST")
apiColls := write.PathPrefix("/api/collections/").Subrouter()
apiColls.HandleFunc("/{alias:[0-9a-zA-Z\\-]+}", handler.AllReader(fetchCollection)).Methods("GET")
apiColls.HandleFunc("/{alias:[0-9a-zA-Z\\-]+}", handler.All(existingCollection)).Methods("POST", "DELETE")
apiColls.HandleFunc("/{alias}/posts", handler.AllReader(fetchCollectionPosts)).Methods("GET")
apiColls.HandleFunc("/{alias}/posts", handler.All(newPost)).Methods("POST")
apiColls.HandleFunc("/{alias}/posts/{post}", handler.AllReader(fetchPost)).Methods("GET")
apiColls.HandleFunc("/{alias}/posts/{post:[a-zA-Z0-9]{10}}", handler.All(existingPost)).Methods("POST")
apiColls.HandleFunc("/{alias}/posts/{post}/{property}", handler.AllReader(fetchPostProperty)).Methods("GET")
apiColls.HandleFunc("/{alias}/collect", handler.All(addPost)).Methods("POST")
apiColls.HandleFunc("/{alias}/pin", handler.All(pinPost)).Methods("POST")
apiColls.HandleFunc("/{alias}/unpin", handler.All(pinPost)).Methods("POST")
apiColls.HandleFunc("/{alias}/inbox", handler.All(handleFetchCollectionInbox)).Methods("POST")
apiColls.HandleFunc("/{alias}/outbox", handler.AllReader(handleFetchCollectionOutbox)).Methods("GET")
apiColls.HandleFunc("/{alias}/following", handler.AllReader(handleFetchCollectionFollowing)).Methods("GET")
apiColls.HandleFunc("/{alias}/followers", handler.AllReader(handleFetchCollectionFollowers)).Methods("GET")
// Handle posts
write.HandleFunc("/api/posts", handler.All(newPost)).Methods("POST")
posts := write.PathPrefix("/api/posts/").Subrouter()
posts.HandleFunc("/{post:[a-zA-Z0-9]{10}}", handler.AllReader(fetchPost)).Methods("GET")
posts.HandleFunc("/{post:[a-zA-Z0-9]{10}}", handler.All(existingPost)).Methods("POST", "PUT")
posts.HandleFunc("/{post:[a-zA-Z0-9]{10}}", handler.All(deletePost)).Methods("DELETE")
posts.HandleFunc("/{post:[a-zA-Z0-9]{10}}/{property}", handler.AllReader(fetchPostProperty)).Methods("GET")
posts.HandleFunc("/claim", handler.All(addPost)).Methods("POST")
posts.HandleFunc("/disperse", handler.All(dispersePost)).Methods("POST")
write.HandleFunc("/auth/signup", handler.Web(handleWebSignup, UserLevelNoneRequired)).Methods("POST")
write.HandleFunc("/auth/login", handler.Web(webLogin, UserLevelNoneRequired)).Methods("POST")
write.HandleFunc("/admin", handler.Admin(handleViewAdminDash)).Methods("GET")
+ write.HandleFunc("/admin/monitor", handler.Admin(handleViewAdminMonitor)).Methods("GET")
+ write.HandleFunc("/admin/settings", handler.Admin(handleViewAdminSettings)).Methods("GET")
write.HandleFunc("/admin/users", handler.Admin(handleViewAdminUsers)).Methods("GET")
write.HandleFunc("/admin/user/{username}", handler.Admin(handleViewAdminUser)).Methods("GET")
write.HandleFunc("/admin/user/{username}/status", handler.Admin(handleAdminToggleUserStatus)).Methods("POST")
write.HandleFunc("/admin/user/{username}/passphrase", handler.Admin(handleAdminResetUserPass)).Methods("POST")
write.HandleFunc("/admin/pages", handler.Admin(handleViewAdminPages)).Methods("GET")
write.HandleFunc("/admin/page/{slug}", handler.Admin(handleViewAdminPage)).Methods("GET")
write.HandleFunc("/admin/update/config", handler.AdminApper(handleAdminUpdateConfig)).Methods("POST")
write.HandleFunc("/admin/update/{page}", handler.Admin(handleAdminUpdateSite)).Methods("POST")
// Handle special pages first
write.HandleFunc("/login", handler.Web(viewLogin, UserLevelNoneRequired))
write.HandleFunc("/signup", handler.Web(handleViewLanding, UserLevelNoneRequired))
write.HandleFunc("/invite/{code:[a-zA-Z0-9]+}", handler.Web(handleViewInvite, UserLevelOptional)).Methods("GET")
// TODO: show a reader-specific 404 page if the function is disabled
write.HandleFunc("/read", handler.Web(viewLocalTimeline, UserLevelReader))
RouteRead(handler, UserLevelReader, write.PathPrefix("/read").Subrouter())
draftEditPrefix := ""
if apper.App().cfg.App.SingleUser {
draftEditPrefix = "/d"
write.HandleFunc("/me/new", handler.Web(handleViewPad, UserLevelUser)).Methods("GET")
} else {
write.HandleFunc("/new", handler.Web(handleViewPad, UserLevelUser)).Methods("GET")
}
// All the existing stuff
write.HandleFunc(draftEditPrefix+"/{action}/edit", handler.Web(handleViewPad, UserLevelUser)).Methods("GET")
write.HandleFunc(draftEditPrefix+"/{action}/meta", handler.Web(handleViewMeta, UserLevelUser)).Methods("GET")
// Collections
if apper.App().cfg.App.SingleUser {
RouteCollections(handler, write.PathPrefix("/").Subrouter())
} else {
write.HandleFunc("/{prefix:[@~$!\\-+]}{collection}", handler.Web(handleViewCollection, UserLevelReader))
write.HandleFunc("/{collection}/", handler.Web(handleViewCollection, UserLevelReader))
RouteCollections(handler, write.PathPrefix("/{prefix:[@~$!\\-+]?}{collection}").Subrouter())
// Posts
}
write.HandleFunc(draftEditPrefix+"/{post}", handler.Web(handleViewPost, UserLevelOptional))
write.HandleFunc("/", handler.Web(handleViewHome, UserLevelOptional))
return r
}
func RouteCollections(handler *Handler, r *mux.Router) {
r.HandleFunc("/page/{page:[0-9]+}", handler.Web(handleViewCollection, UserLevelReader))
r.HandleFunc("/tag:{tag}", handler.Web(handleViewCollectionTag, UserLevelReader))
r.HandleFunc("/tag:{tag}/feed/", handler.Web(ViewFeed, UserLevelReader))
r.HandleFunc("/tags/{tag}", handler.Web(handleViewCollectionTag, UserLevelReader))
r.HandleFunc("/sitemap.xml", handler.AllReader(handleViewSitemap))
r.HandleFunc("/feed/", handler.AllReader(ViewFeed))
r.HandleFunc("/{slug}", handler.CollectionPostOrStatic)
r.HandleFunc("/{slug}/edit", handler.Web(handleViewPad, UserLevelUser))
r.HandleFunc("/{slug}/edit/meta", handler.Web(handleViewMeta, UserLevelUser))
r.HandleFunc("/{slug}/", handler.Web(handleCollectionPostRedirect, UserLevelReader)).Methods("GET")
}
func RouteRead(handler *Handler, readPerm UserLevelFunc, r *mux.Router) {
r.HandleFunc("/api/posts", handler.Web(viewLocalTimelineAPI, readPerm))
r.HandleFunc("/p/{page}", handler.Web(viewLocalTimeline, readPerm))
r.HandleFunc("/feed/", handler.Web(viewLocalTimelineFeed, readPerm))
r.HandleFunc("/t/{tag}", handler.Web(viewLocalTimeline, readPerm))
r.HandleFunc("/a/{post}", handler.Web(handlePostIDRedirect, readPerm))
r.HandleFunc("/{author}", handler.Web(viewLocalTimeline, readPerm))
r.HandleFunc("/", handler.Web(viewLocalTimeline, readPerm))
}
diff --git a/templates/user/admin.tmpl b/templates/user/admin.tmpl
index 3cb81be..1a0e6b4 100644
--- a/templates/user/admin.tmpl
+++ b/templates/user/admin.tmpl
@@ -1,190 +1,68 @@
{{define "admin"}}
{{template "header" .}}
<style type="text/css">
h2 {font-weight: normal;}
ul.pagenav {list-style: none;}
form {
margin: 0 0 2em;
}
form dt {
line-height: inherit;
}
.ui.divider:not(.vertical):not(.horizontal) {
border-top: 1px solid rgba(34,36,38,.15);
border-bottom: 1px solid rgba(255,255,255,.1);
}
.ui.divider {
margin: 1rem 0;
line-height: 1;
height: 0;
font-weight: 700;
text-transform: uppercase;
letter-spacing: .05em;
color: rgba(0,0,0,.85);
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-tap-highlight-color: transparent;
font-size: 1rem;
}
.invisible {
display: none;
}
p.docs {
font-size: 0.86em;
}
+.stats {
+ font-size: 1.2em;
+ margin: 1em 0;
+}
+.num {
+ font-weight: bold;
+ font-size: 1.5em;
+}
</style>
<div class="content-container snug">
{{template "admin-header" .}}
{{if .Message}}<p>{{.Message}}</p>{{end}}
- <h2>On this page</h2>
- <ul class="pagenav">
- <li><a href="#config">Configuration</a></li>
- <li><a href="#monitor">Application monitor</a></li>
- </ul>
-
- <h2>Resources</h2>
- <ul class="pagenav">
- <li><a href="https://writefreely.org/docs/{{.OfficialVersion}}/admin">Admin Guide</a></li>
- </ul>
-
- <hr />
-
- <h2><a name="config"></a>App Configuration</h2>
- <p class="docs">Read more in the <a href="https://writefreely.org/docs/{{.OfficialVersion}}/admin/config">configuration docs</a>.</p>
-
- {{if .ConfigMessage}}<p class="success" style="text-align: center">{{.ConfigMessage}}</p>{{end}}
-
- <form action="/admin/update/config" method="post">
- <div class="ui attached table segment">
- <dl class="dl-horizontal admin-dl-horizontal">
- <dt{{if .Config.SingleUser}} class="invisible"{{end}}>Site Name</dt>
- <dd{{if .Config.SingleUser}} class="invisible"{{end}}><input type="text" name="site_name" id="site_name" class="inline" value="{{.Config.SiteName}}" style="width: 14em;" /></dd>
- <dt{{if .Config.SingleUser}} class="invisible"{{end}}>Site Description</dt>
- <dd{{if .Config.SingleUser}} class="invisible"{{end}}><input type="text" name="site_desc" id="site_desc" class="inline" value="{{.Config.SiteDesc}}" style="width: 14em;" /></dd>
- <dt>Host</dt>
- <dd>{{.Config.Host}}</dd>
- <dt>User Mode</dt>
- <dd>{{if .Config.SingleUser}}Single user{{else}}Multiple users{{end}}</dd>
- <dt{{if .Config.SingleUser}} class="invisible"{{end}}>Landing Page</dt>
- <dd{{if .Config.SingleUser}} class="invisible"{{end}}><input type="text" name="landing" id="landing" class="inline" value="{{.Config.Landing}}" style="width: 14em;" /></dd>
- <dt{{if .Config.SingleUser}} class="invisible"{{end}}><label for="open_registration">Open Registrations</label></dt>
- <dd{{if .Config.SingleUser}} class="invisible"{{end}}><input type="checkbox" name="open_registration" id="open_registration" {{if .Config.OpenRegistration}}checked="checked"{{end}} /></dd>
- <dt><label for="min_username_len">Minimum Username Length</label></dt>
- <dd><input type="number" name="min_username_len" id="min_username_len" class="inline" min="1" max="100" value="{{.Config.MinUsernameLen}}" /></dd>
- <dt{{if .Config.SingleUser}} class="invisible"{{end}}><label for="max_blogs">Maximum Blogs per User</label></dt>
- <dd{{if .Config.SingleUser}} class="invisible"{{end}}><input type="number" name="max_blogs" id="max_blogs" class="inline" min="1" value="{{.Config.MaxBlogs}}" /></dd>
- <dt><label for="federation">Federation</label></dt>
- <dd><input type="checkbox" name="federation" id="federation" {{if .Config.Federation}}checked="checked"{{end}} /></dd>
- <dt><label for="public_stats">Public Stats</label></dt>
- <dd><input type="checkbox" name="public_stats" id="public_stats" {{if .Config.PublicStats}}checked="checked"{{end}} /></dd>
- <dt><label for="private">Private Instance</label></dt>
- <dd><input type="checkbox" name="private" id="private" {{if .Config.Private}}checked="checked"{{end}} /></dd>
- <dt{{if .Config.SingleUser}} class="invisible"{{end}}><label for="local_timeline">Local Timeline</label></dt>
- <dd{{if .Config.SingleUser}} class="invisible"{{end}}><input type="checkbox" name="local_timeline" id="local_timeline" {{if .Config.LocalTimeline}}checked="checked"{{end}} /></dd>
- <dt{{if .Config.SingleUser}} class="invisible"{{end}}><label for="user_invites">Allow sending invitations by</label></dt>
- <dd{{if .Config.SingleUser}} class="invisible"{{end}}>
- <select name="user_invites" id="user_invites">
- <option value="none" {{if eq .Config.UserInvites ""}}selected="selected"{{end}}>No one</option>
- <option value="user" {{if eq .Config.UserInvites "user"}}selected="selected"{{end}}>Users</option>
- <option value="admin" {{if eq .Config.UserInvites "admin"}}selected="selected"{{end}}>Admins</option>
- </select>
- </dd>
- <dt{{if .Config.SingleUser}} class="invisible"{{end}}><label for="default_visibility">Default blog visibility</label></dt>
- <dd{{if .Config.SingleUser}} class="invisible"{{end}}>
- <select name="default_visibility" id="default_visibility">
- <option value="unlisted" {{if eq .Config.DefaultVisibility "unlisted"}}selected="selected"{{end}}>Unlisted</option>
- <option value="public" {{if eq .Config.DefaultVisibility "public"}}selected="selected"{{end}}>Public</option>
- <option value="private" {{if eq .Config.DefaultVisibility "private"}}selected="selected"{{end}}>Private</option>
- </select>
- </dd>
- </dl>
- <input type="submit" value="Save Configuration" />
+ <div class="row stats">
+ <div><span class="num">{{largeNumFmt .UsersCount}}</span> {{pluralize "user" "users" .UsersCount}}</div>
+ <div><span class="num">{{largeNumFmt .CollectionsCount}}</span> {{pluralize "blog" "blogs" .CollectionsCount}}</div>
+ <div><span class="num">{{largeNumFmt .PostsCount}}</span> {{pluralize "post" "posts" .PostsCount}}</div>
</div>
- </form>
-
- <hr />
- <h2><a name="monitor"></a>Application</h2>
-
- <div class="ui attached table segment">
- <dl class="dl-horizontal admin-dl-horizontal">
- <dt>WriteFreely</dt>
- <dd>{{.Version}}</dd>
- <dt>Server Uptime</dt>
- <dd>{{.SysStatus.Uptime}}</dd>
- <dt>Current Goroutines</dt>
- <dd>{{.SysStatus.NumGoroutine}}</dd>
- <div class="ui divider"></div>
- <dt>Current memory usage</dt>
- <dd>{{.SysStatus.MemAllocated}}</dd>
- <dt>Total mem allocated</dt>
- <dd>{{.SysStatus.MemTotal}}</dd>
- <dt>Memory obtained</dt>
- <dd>{{.SysStatus.MemSys}}</dd>
- <dt>Pointer lookup times</dt>
- <dd>{{.SysStatus.Lookups}}</dd>
- <dt>Memory allocate times</dt>
- <dd>{{.SysStatus.MemMallocs}}</dd>
- <dt>Memory free times</dt>
- <dd>{{.SysStatus.MemFrees}}</dd>
- <div class="ui divider"></div>
- <dt>Current heap usage</dt>
- <dd>{{.SysStatus.HeapAlloc}}</dd>
- <dt>Heap memory obtained</dt>
- <dd>{{.SysStatus.HeapSys}}</dd>
- <dt>Heap memory idle</dt>
- <dd>{{.SysStatus.HeapIdle}}</dd>
- <dt>Heap memory in use</dt>
- <dd>{{.SysStatus.HeapInuse}}</dd>
- <dt>Heap memory released</dt>
- <dd>{{.SysStatus.HeapReleased}}</dd>
- <dt>Heap objects</dt>
- <dd>{{.SysStatus.HeapObjects}}</dd>
- <div class="ui divider"></div>
- <dt>Bootstrap stack usage</dt>
- <dd>{{.SysStatus.StackInuse}}</dd>
- <dt>Stack memory obtained</dt>
- <dd>{{.SysStatus.StackSys}}</dd>
- <dt>MSpan structures in use</dt>
- <dd>{{.SysStatus.MSpanInuse}}</dd>
- <dt>MSpan structures obtained</dt>
- <dd>{{.SysStatus.HeapSys}}</dd>
- <dt>MCache structures in use</dt>
- <dd>{{.SysStatus.MCacheInuse}}</dd>
- <dt>MCache structures obtained</dt>
- <dd>{{.SysStatus.MCacheSys}}</dd>
- <dt>Profiling bucket hash table obtained</dt>
- <dd>{{.SysStatus.BuckHashSys}}</dd>
- <dt>GC metadata obtained</dt>
- <dd>{{.SysStatus.GCSys}}</dd>
- <dt>Other system allocation obtained</dt>
- <dd>{{.SysStatus.OtherSys}}</dd>
- <div class="ui divider"></div>
- <dt>Next GC recycle</dt>
- <dd>{{.SysStatus.NextGC}}</dd>
- <dt>Since last GC</dt>
- <dd>{{.SysStatus.LastGC}}</dd>
- <dt>Total GC pause</dt>
- <dd>{{.SysStatus.PauseTotalNs}}</dd>
- <dt>Last GC pause</dt>
- <dd>{{.SysStatus.PauseNs}}</dd>
- <dt>GC times</dt>
- <dd>{{.SysStatus.NumGC}}</dd>
- </dl>
- </div>
</div>
<script>
history.replaceState(null, "", "/admin"+window.location.hash);
</script>
{{template "footer" .}}
{{template "body-end" .}}
{{end}}
diff --git a/templates/user/admin.tmpl b/templates/user/admin/app-settings.tmpl
similarity index 57%
copy from templates/user/admin.tmpl
copy to templates/user/admin/app-settings.tmpl
index 3cb81be..4af6a44 100644
--- a/templates/user/admin.tmpl
+++ b/templates/user/admin/app-settings.tmpl
@@ -1,190 +1,85 @@
-{{define "admin"}}
+{{define "app-settings"}}
{{template "header" .}}
<style type="text/css">
h2 {font-weight: normal;}
-ul.pagenav {list-style: none;}
form {
margin: 0 0 2em;
}
form dt {
line-height: inherit;
}
-.ui.divider:not(.vertical):not(.horizontal) {
- border-top: 1px solid rgba(34,36,38,.15);
- border-bottom: 1px solid rgba(255,255,255,.1);
-}
-.ui.divider {
- margin: 1rem 0;
- line-height: 1;
- height: 0;
- font-weight: 700;
- text-transform: uppercase;
- letter-spacing: .05em;
- color: rgba(0,0,0,.85);
- -webkit-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
- -webkit-tap-highlight-color: transparent;
- font-size: 1rem;
-}
.invisible {
display: none;
}
p.docs {
font-size: 0.86em;
}
</style>
<div class="content-container snug">
{{template "admin-header" .}}
- {{if .Message}}<p>{{.Message}}</p>{{end}}
-
- <h2>On this page</h2>
- <ul class="pagenav">
- <li><a href="#config">Configuration</a></li>
- <li><a href="#monitor">Application monitor</a></li>
- </ul>
-
- <h2>Resources</h2>
- <ul class="pagenav">
- <li><a href="https://writefreely.org/docs/{{.OfficialVersion}}/admin">Admin Guide</a></li>
- </ul>
+ {{if .Message}}<p><a name="config"></a>{{.Message}}</p>{{end}}
- <hr />
+ {{if .ConfigMessage}}<p class="success" style="text-align: center">{{.ConfigMessage}}</p>{{end}}
- <h2><a name="config"></a>App Configuration</h2>
<p class="docs">Read more in the <a href="https://writefreely.org/docs/{{.OfficialVersion}}/admin/config">configuration docs</a>.</p>
- {{if .ConfigMessage}}<p class="success" style="text-align: center">{{.ConfigMessage}}</p>{{end}}
-
<form action="/admin/update/config" method="post">
<div class="ui attached table segment">
<dl class="dl-horizontal admin-dl-horizontal">
<dt{{if .Config.SingleUser}} class="invisible"{{end}}>Site Name</dt>
<dd{{if .Config.SingleUser}} class="invisible"{{end}}><input type="text" name="site_name" id="site_name" class="inline" value="{{.Config.SiteName}}" style="width: 14em;" /></dd>
<dt{{if .Config.SingleUser}} class="invisible"{{end}}>Site Description</dt>
<dd{{if .Config.SingleUser}} class="invisible"{{end}}><input type="text" name="site_desc" id="site_desc" class="inline" value="{{.Config.SiteDesc}}" style="width: 14em;" /></dd>
<dt>Host</dt>
<dd>{{.Config.Host}}</dd>
<dt>User Mode</dt>
<dd>{{if .Config.SingleUser}}Single user{{else}}Multiple users{{end}}</dd>
<dt{{if .Config.SingleUser}} class="invisible"{{end}}>Landing Page</dt>
<dd{{if .Config.SingleUser}} class="invisible"{{end}}><input type="text" name="landing" id="landing" class="inline" value="{{.Config.Landing}}" style="width: 14em;" /></dd>
<dt{{if .Config.SingleUser}} class="invisible"{{end}}><label for="open_registration">Open Registrations</label></dt>
<dd{{if .Config.SingleUser}} class="invisible"{{end}}><input type="checkbox" name="open_registration" id="open_registration" {{if .Config.OpenRegistration}}checked="checked"{{end}} /></dd>
<dt><label for="min_username_len">Minimum Username Length</label></dt>
<dd><input type="number" name="min_username_len" id="min_username_len" class="inline" min="1" max="100" value="{{.Config.MinUsernameLen}}" /></dd>
<dt{{if .Config.SingleUser}} class="invisible"{{end}}><label for="max_blogs">Maximum Blogs per User</label></dt>
<dd{{if .Config.SingleUser}} class="invisible"{{end}}><input type="number" name="max_blogs" id="max_blogs" class="inline" min="1" value="{{.Config.MaxBlogs}}" /></dd>
<dt><label for="federation">Federation</label></dt>
<dd><input type="checkbox" name="federation" id="federation" {{if .Config.Federation}}checked="checked"{{end}} /></dd>
<dt><label for="public_stats">Public Stats</label></dt>
<dd><input type="checkbox" name="public_stats" id="public_stats" {{if .Config.PublicStats}}checked="checked"{{end}} /></dd>
<dt><label for="private">Private Instance</label></dt>
<dd><input type="checkbox" name="private" id="private" {{if .Config.Private}}checked="checked"{{end}} /></dd>
<dt{{if .Config.SingleUser}} class="invisible"{{end}}><label for="local_timeline">Local Timeline</label></dt>
<dd{{if .Config.SingleUser}} class="invisible"{{end}}><input type="checkbox" name="local_timeline" id="local_timeline" {{if .Config.LocalTimeline}}checked="checked"{{end}} /></dd>
<dt{{if .Config.SingleUser}} class="invisible"{{end}}><label for="user_invites">Allow sending invitations by</label></dt>
<dd{{if .Config.SingleUser}} class="invisible"{{end}}>
<select name="user_invites" id="user_invites">
<option value="none" {{if eq .Config.UserInvites ""}}selected="selected"{{end}}>No one</option>
<option value="user" {{if eq .Config.UserInvites "user"}}selected="selected"{{end}}>Users</option>
<option value="admin" {{if eq .Config.UserInvites "admin"}}selected="selected"{{end}}>Admins</option>
</select>
</dd>
<dt{{if .Config.SingleUser}} class="invisible"{{end}}><label for="default_visibility">Default blog visibility</label></dt>
<dd{{if .Config.SingleUser}} class="invisible"{{end}}>
<select name="default_visibility" id="default_visibility">
<option value="unlisted" {{if eq .Config.DefaultVisibility "unlisted"}}selected="selected"{{end}}>Unlisted</option>
<option value="public" {{if eq .Config.DefaultVisibility "public"}}selected="selected"{{end}}>Public</option>
<option value="private" {{if eq .Config.DefaultVisibility "private"}}selected="selected"{{end}}>Private</option>
</select>
</dd>
</dl>
- <input type="submit" value="Save Configuration" />
+ <input type="submit" value="Save Settings" />
</div>
</form>
-
- <hr />
-
- <h2><a name="monitor"></a>Application</h2>
-
- <div class="ui attached table segment">
- <dl class="dl-horizontal admin-dl-horizontal">
- <dt>WriteFreely</dt>
- <dd>{{.Version}}</dd>
- <dt>Server Uptime</dt>
- <dd>{{.SysStatus.Uptime}}</dd>
- <dt>Current Goroutines</dt>
- <dd>{{.SysStatus.NumGoroutine}}</dd>
- <div class="ui divider"></div>
- <dt>Current memory usage</dt>
- <dd>{{.SysStatus.MemAllocated}}</dd>
- <dt>Total mem allocated</dt>
- <dd>{{.SysStatus.MemTotal}}</dd>
- <dt>Memory obtained</dt>
- <dd>{{.SysStatus.MemSys}}</dd>
- <dt>Pointer lookup times</dt>
- <dd>{{.SysStatus.Lookups}}</dd>
- <dt>Memory allocate times</dt>
- <dd>{{.SysStatus.MemMallocs}}</dd>
- <dt>Memory free times</dt>
- <dd>{{.SysStatus.MemFrees}}</dd>
- <div class="ui divider"></div>
- <dt>Current heap usage</dt>
- <dd>{{.SysStatus.HeapAlloc}}</dd>
- <dt>Heap memory obtained</dt>
- <dd>{{.SysStatus.HeapSys}}</dd>
- <dt>Heap memory idle</dt>
- <dd>{{.SysStatus.HeapIdle}}</dd>
- <dt>Heap memory in use</dt>
- <dd>{{.SysStatus.HeapInuse}}</dd>
- <dt>Heap memory released</dt>
- <dd>{{.SysStatus.HeapReleased}}</dd>
- <dt>Heap objects</dt>
- <dd>{{.SysStatus.HeapObjects}}</dd>
- <div class="ui divider"></div>
- <dt>Bootstrap stack usage</dt>
- <dd>{{.SysStatus.StackInuse}}</dd>
- <dt>Stack memory obtained</dt>
- <dd>{{.SysStatus.StackSys}}</dd>
- <dt>MSpan structures in use</dt>
- <dd>{{.SysStatus.MSpanInuse}}</dd>
- <dt>MSpan structures obtained</dt>
- <dd>{{.SysStatus.HeapSys}}</dd>
- <dt>MCache structures in use</dt>
- <dd>{{.SysStatus.MCacheInuse}}</dd>
- <dt>MCache structures obtained</dt>
- <dd>{{.SysStatus.MCacheSys}}</dd>
- <dt>Profiling bucket hash table obtained</dt>
- <dd>{{.SysStatus.BuckHashSys}}</dd>
- <dt>GC metadata obtained</dt>
- <dd>{{.SysStatus.GCSys}}</dd>
- <dt>Other system allocation obtained</dt>
- <dd>{{.SysStatus.OtherSys}}</dd>
- <div class="ui divider"></div>
- <dt>Next GC recycle</dt>
- <dd>{{.SysStatus.NextGC}}</dd>
- <dt>Since last GC</dt>
- <dd>{{.SysStatus.LastGC}}</dd>
- <dt>Total GC pause</dt>
- <dd>{{.SysStatus.PauseTotalNs}}</dd>
- <dt>Last GC pause</dt>
- <dd>{{.SysStatus.PauseNs}}</dd>
- <dt>GC times</dt>
- <dd>{{.SysStatus.NumGC}}</dd>
- </dl>
- </div>
</div>
<script>
-history.replaceState(null, "", "/admin"+window.location.hash);
+history.replaceState(null, "", "/admin/settings"+window.location.hash);
</script>
{{template "footer" .}}
{{template "body-end" .}}
{{end}}
diff --git a/templates/user/admin/monitor.tmpl b/templates/user/admin/monitor.tmpl
new file mode 100644
index 0000000..e803dd3
--- /dev/null
+++ b/templates/user/admin/monitor.tmpl
@@ -0,0 +1,105 @@
+{{define "monitor"}}
+{{template "header" .}}
+
+<style type="text/css">
+h2 {font-weight: normal;}
+.ui.divider:not(.vertical):not(.horizontal) {
+ border-top: 1px solid rgba(34,36,38,.15);
+ border-bottom: 1px solid rgba(255,255,255,.1);
+}
+.ui.divider {
+ margin: 1rem 0;
+ line-height: 1;
+ height: 0;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: .05em;
+ color: rgba(0,0,0,.85);
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ -webkit-tap-highlight-color: transparent;
+ font-size: 1rem;
+}
+</style>
+
+<div class="content-container snug">
+ {{template "admin-header" .}}
+
+ {{if .Message}}<p>{{.Message}}</p>{{end}}
+
+ <h2><a name="monitor"></a>Application Monitor</h2>
+
+ <div class="ui attached table segment">
+ <dl class="dl-horizontal admin-dl-horizontal">
+ <dt>WriteFreely</dt>
+ <dd>{{.Version}}</dd>
+ <dt>Server Uptime</dt>
+ <dd>{{.SysStatus.Uptime}}</dd>
+ <dt>Current Goroutines</dt>
+ <dd>{{.SysStatus.NumGoroutine}}</dd>
+ <div class="ui divider"></div>
+ <dt>Current memory usage</dt>
+ <dd>{{.SysStatus.MemAllocated}}</dd>
+ <dt>Total mem allocated</dt>
+ <dd>{{.SysStatus.MemTotal}}</dd>
+ <dt>Memory obtained</dt>
+ <dd>{{.SysStatus.MemSys}}</dd>
+ <dt>Pointer lookup times</dt>
+ <dd>{{.SysStatus.Lookups}}</dd>
+ <dt>Memory allocate times</dt>
+ <dd>{{.SysStatus.MemMallocs}}</dd>
+ <dt>Memory free times</dt>
+ <dd>{{.SysStatus.MemFrees}}</dd>
+ <div class="ui divider"></div>
+ <dt>Current heap usage</dt>
+ <dd>{{.SysStatus.HeapAlloc}}</dd>
+ <dt>Heap memory obtained</dt>
+ <dd>{{.SysStatus.HeapSys}}</dd>
+ <dt>Heap memory idle</dt>
+ <dd>{{.SysStatus.HeapIdle}}</dd>
+ <dt>Heap memory in use</dt>
+ <dd>{{.SysStatus.HeapInuse}}</dd>
+ <dt>Heap memory released</dt>
+ <dd>{{.SysStatus.HeapReleased}}</dd>
+ <dt>Heap objects</dt>
+ <dd>{{.SysStatus.HeapObjects}}</dd>
+ <div class="ui divider"></div>
+ <dt>Bootstrap stack usage</dt>
+ <dd>{{.SysStatus.StackInuse}}</dd>
+ <dt>Stack memory obtained</dt>
+ <dd>{{.SysStatus.StackSys}}</dd>
+ <dt>MSpan structures in use</dt>
+ <dd>{{.SysStatus.MSpanInuse}}</dd>
+ <dt>MSpan structures obtained</dt>
+ <dd>{{.SysStatus.HeapSys}}</dd>
+ <dt>MCache structures in use</dt>
+ <dd>{{.SysStatus.MCacheInuse}}</dd>
+ <dt>MCache structures obtained</dt>
+ <dd>{{.SysStatus.MCacheSys}}</dd>
+ <dt>Profiling bucket hash table obtained</dt>
+ <dd>{{.SysStatus.BuckHashSys}}</dd>
+ <dt>GC metadata obtained</dt>
+ <dd>{{.SysStatus.GCSys}}</dd>
+ <dt>Other system allocation obtained</dt>
+ <dd>{{.SysStatus.OtherSys}}</dd>
+ <div class="ui divider"></div>
+ <dt>Next GC recycle</dt>
+ <dd>{{.SysStatus.NextGC}}</dd>
+ <dt>Since last GC</dt>
+ <dd>{{.SysStatus.LastGC}}</dd>
+ <dt>Total GC pause</dt>
+ <dd>{{.SysStatus.PauseTotalNs}}</dd>
+ <dt>Last GC pause</dt>
+ <dd>{{.SysStatus.PauseNs}}</dd>
+ <dt>GC times</dt>
+ <dd>{{.SysStatus.NumGC}}</dd>
+ </dl>
+ </div>
+</div>
+
+{{template "footer" .}}
+
+{{template "body-end" .}}
+{{end}}
diff --git a/templates/user/include/header.tmpl b/templates/user/include/header.tmpl
index 0704854..f7c06ef 100644
--- a/templates/user/include/header.tmpl
+++ b/templates/user/include/header.tmpl
@@ -1,108 +1,110 @@
{{define "user-navigation"}}
<header class="{{if .SingleUser}}singleuser{{else}}multiuser{{end}}">
{{if .SingleUser}}
<nav id="user-nav">
<nav class="dropdown-nav">
<ul><li><a href="/" title="View blog" class="title">{{.SiteName}}</a> <img class="ic-18dp" src="/img/ic_down_arrow_dark@2x.png" />
<ul>
<li><a href="/me/c/{{.Username}}">Customize</a></li>
<li><a href="/me/c/{{.Username}}/stats">Stats</a></li>
<li class="separator"><hr /></li>
{{if .IsAdmin}}<li><a href="/admin">Admin</a></li>{{end}}
<li><a href="/me/settings">Settings</a></li>
<li><a href="/me/import">Import posts</a></li>
<li><a href="/me/export">Export</a></li>
<li class="separator"><hr /></li>
<li><a href="/me/logout">Log out</a></li>
</ul></li>
</ul>
</nav>
<nav class="tabs">
<a href="/me/posts/"{{if eq .Path "/me/posts/"}} class="selected"{{end}}>Drafts</a>
<a href="/me/new">New Post</a>
</nav>
</nav>
{{else}}
<nav id="full-nav">
<div class="left-side">
<h1><a href="/" title="Return to editor">{{.SiteName}}</a></h1>
</div>
<nav id="user-nav">
{{if .Username}}
<nav class="dropdown-nav">
<ul><li><a>{{.Username}}</a> <img class="ic-18dp" src="/img/ic_down_arrow_dark@2x.png" /><ul>
{{if .IsAdmin}}<li><a href="/admin">Admin dashboard</a></li>{{end}}
<li><a href="/me/settings">Account settings</a></li>
<li><a href="/me/import">Import posts</a></li>
<li><a href="/me/export">Export</a></li>
{{if .CanInvite}}<li><a href="/me/invites">Invite people</a></li>{{end}}
<li class="separator"><hr /></li>
<li><a href="/me/logout">Log out</a></li>
</ul></li>
</ul>
</nav>
{{end}}
<nav class="tabs">
{{if .SimpleNav}}
{{ if not .SingleUser }}
{{if and (and .LocalTimeline .CanViewReader) .Chorus}}<a href="/"{{if eq .Path "/"}} class="selected"{{end}}>Home</a>{{end}}
{{ end }}
<a href="/about">About</a>
{{ if not .SingleUser }}
{{ if .Username }}
{{if gt .MaxBlogs 1}}<a href="/me/c/"{{if eq .Path "/me/c/"}} class="selected"{{end}}>Blogs</a>{{end}}
{{if and .Chorus (eq .MaxBlogs 1)}}<a href="/{{.Username}}/"{{if eq .Path (printf "/%s/" .Username)}} class="selected"{{end}}>My Posts</a>{{end}}
{{if not .DisableDrafts}}<a href="/me/posts/"{{if eq .Path "/me/posts/"}} class="selected"{{end}}>Drafts</a>{{end}}
{{ end }}
{{if and (and .LocalTimeline .CanViewReader) (not .Chorus)}}<a href="/read">Reader</a>{{end}}
{{if and (and (and .Chorus .OpenRegistration) (not .Username)) (or (not .Private) (ne .Landing ""))}}<a href="/signup"{{if eq .Path "/signup"}} class="selected"{{end}}>Sign up</a>{{end}}
{{if .Username}}<a href="/me/logout">Log out</a>{{else}}<a href="/login">Log in</a>{{end}}
{{ end }}
{{else}}
<a href="/me/c/"{{if eq .Path "/me/c/"}} class="selected"{{end}}>Blogs</a>
{{if not .DisableDrafts}}<a href="/me/posts/"{{if eq .Path "/me/posts/"}} class="selected"{{end}}>Drafts</a>{{end}}
{{if and (and .LocalTimeline .CanViewReader) (not .Chorus)}}<a href="/read">Reader</a>{{end}}
{{end}}
</nav>
</nav>
{{if .Chorus}}{{if .Username}}<div class="right-side">
<a class="simple-btn" href="/new">New Post</a>
</div>{{end}}
</nav>
{{end}}
{{end}}
</header>
{{end}}
{{define "header"}}<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>{{.PageTitle}} {{if .Separator}}{{.Separator}}{{else}}—{{end}} {{.SiteName}}</title>
<link rel="stylesheet" type="text/css" href="/css/write.css" />
<link rel="shortcut icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#888888" />
<meta name="apple-mobile-web-app-title" content="{{.SiteName}}">
<link rel="apple-touch-icon" sizes="152x152" href="/img/touch-icon-152.png">
<link rel="apple-touch-icon" sizes="167x167" href="/img/touch-icon-167.png">
<link rel="apple-touch-icon" sizes="180x180" href="/img/touch-icon-180.png">
</head>
<body id="me">
{{template "user-navigation" .}}
<div id="official-writing">
{{end}}
{{define "admin-header"}}
<header class="admin">
<h1>Admin</h1>
- <nav id="admin">
+ <nav id="admin" class="pager">
<a href="/admin" {{if eq .Path "/admin"}}class="selected"{{end}}>Dashboard</a>
+ <a href="/admin/settings" {{if eq .Path "/admin/settings"}}class="selected"{{end}}>Settings</a>
{{if not .SingleUser}}
<a href="/admin/users" {{if eq .Path "/admin/users"}}class="selected"{{end}}>Users</a>
<a href="/admin/pages" {{if eq .Path "/admin/pages"}}class="selected"{{end}}>Pages</a>
{{end}}
+ <a href="/admin/monitor" {{if eq .Path "/admin/monitor"}}class="selected"{{end}}>Monitor</a>
</nav>
</header>
{{end}}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sat, Feb 1, 1:41 AM (17 h, 39 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3146019
Attached To
rWF WriteFreely
Event Timeline
Log In to Comment