@@ -34,17 +34,19 @@ type ( | |||
PageTitle string | |||
Separator template.HTML | |||
IsAdmin bool | |||
} | |||
) | |||
func NewUserPage(app *app, r *http.Request, username, title string, flashes []string) *UserPage { | |||
func NewUserPage(app *app, r *http.Request, u *User, title string, flashes []string) *UserPage { | |||
up := &UserPage{ | |||
StaticPage: pageForReq(app, r), | |||
PageTitle: title, | |||
} | |||
up.Username = username | |||
up.Username = u.Username | |||
up.Flashes = flashes | |||
up.Path = r.URL.Path | |||
up.IsAdmin = u.IsAdmin() | |||
return up | |||
} | |||
@@ -538,7 +540,7 @@ func getVerboseAuthUser(app *app, token string, u *User, verbose bool) *AuthUser | |||
func viewExportOptions(app *app, u *User, w http.ResponseWriter, r *http.Request) error { | |||
// Fetch extra user data | |||
p := NewUserPage(app, r, u.Username, "Export", nil) | |||
p := NewUserPage(app, r, u, "Export", nil) | |||
showUserPage(w, "export", p) | |||
return nil | |||
@@ -722,7 +724,7 @@ func viewArticles(app *app, u *User, w http.ResponseWriter, r *http.Request) err | |||
AnonymousPosts *[]PublicPost | |||
Collections *[]Collection | |||
}{ | |||
UserPage: NewUserPage(app, r, u.Username, u.Username+"'s Posts", f), | |||
UserPage: NewUserPage(app, r, u, u.Username+"'s Posts", f), | |||
AnonymousPosts: p, | |||
Collections: c, | |||
} | |||
@@ -754,7 +756,7 @@ func viewCollections(app *app, u *User, w http.ResponseWriter, r *http.Request) | |||
NewBlogsDisabled bool | |||
}{ | |||
UserPage: NewUserPage(app, r, u.Username, u.Username+"'s Blogs", f), | |||
UserPage: NewUserPage(app, r, u, u.Username+"'s Blogs", f), | |||
Collections: c, | |||
UsedCollections: int(uc), | |||
NewBlogsDisabled: !app.cfg.App.CanCreateBlogs(uc), | |||
@@ -780,7 +782,7 @@ func viewEditCollection(app *app, u *User, w http.ResponseWriter, r *http.Reques | |||
*UserPage | |||
*Collection | |||
}{ | |||
UserPage: NewUserPage(app, r, u.Username, "Edit "+c.DisplayTitle(), flashes), | |||
UserPage: NewUserPage(app, r, u, "Edit "+c.DisplayTitle(), flashes), | |||
Collection: c, | |||
} | |||
@@ -952,7 +954,7 @@ func viewStats(app *app, u *User, w http.ResponseWriter, r *http.Request) error | |||
TopPosts *[]PublicPost | |||
APFollowers int | |||
}{ | |||
UserPage: NewUserPage(app, r, u.Username, titleStats+"Stats", flashes), | |||
UserPage: NewUserPage(app, r, u, titleStats+"Stats", flashes), | |||
VisitsBlog: alias, | |||
Collection: c, | |||
TopPosts: topPosts, | |||
@@ -990,7 +992,7 @@ func viewSettings(app *app, u *User, w http.ResponseWriter, r *http.Request) err | |||
HasPass bool | |||
IsLogOut bool | |||
}{ | |||
UserPage: NewUserPage(app, r, u.Username, "Account Settings", flashes), | |||
UserPage: NewUserPage(app, r, u, "Account Settings", flashes), | |||
Email: fullUser.EmailClear(app.keys), | |||
HasPass: passIsSet, | |||
IsLogOut: r.FormValue("logout") == "1", | |||
@@ -2,11 +2,114 @@ package writefreely | |||
import ( | |||
"fmt" | |||
"github.com/gogits/gogs/pkg/tool" | |||
"github.com/writeas/impart" | |||
"github.com/writeas/web-core/auth" | |||
"net/http" | |||
"runtime" | |||
"time" | |||
) | |||
var ( | |||
appStartTime = time.Now() | |||
sysStatus systemStatus | |||
) | |||
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 | |||
} | |||
func handleViewAdminDash(app *app, u *User, w http.ResponseWriter, r *http.Request) error { | |||
updateAppStats() | |||
p := struct { | |||
*UserPage | |||
Message string | |||
SysStatus systemStatus | |||
}{ | |||
NewUserPage(app, r, u, "Admin", nil), | |||
r.FormValue("m"), | |||
sysStatus, | |||
} | |||
showUserPage(w, "admin", p) | |||
return nil | |||
} | |||
func updateAppStats() { | |||
sysStatus.Uptime = tool.TimeSincePro(appStartTime) | |||
m := new(runtime.MemStats) | |||
runtime.ReadMemStats(m) | |||
sysStatus.NumGoroutine = runtime.NumGoroutine() | |||
sysStatus.MemAllocated = tool.FileSize(int64(m.Alloc)) | |||
sysStatus.MemTotal = tool.FileSize(int64(m.TotalAlloc)) | |||
sysStatus.MemSys = tool.FileSize(int64(m.Sys)) | |||
sysStatus.Lookups = m.Lookups | |||
sysStatus.MemMallocs = m.Mallocs | |||
sysStatus.MemFrees = m.Frees | |||
sysStatus.HeapAlloc = tool.FileSize(int64(m.HeapAlloc)) | |||
sysStatus.HeapSys = tool.FileSize(int64(m.HeapSys)) | |||
sysStatus.HeapIdle = tool.FileSize(int64(m.HeapIdle)) | |||
sysStatus.HeapInuse = tool.FileSize(int64(m.HeapInuse)) | |||
sysStatus.HeapReleased = tool.FileSize(int64(m.HeapReleased)) | |||
sysStatus.HeapObjects = m.HeapObjects | |||
sysStatus.StackInuse = tool.FileSize(int64(m.StackInuse)) | |||
sysStatus.StackSys = tool.FileSize(int64(m.StackSys)) | |||
sysStatus.MSpanInuse = tool.FileSize(int64(m.MSpanInuse)) | |||
sysStatus.MSpanSys = tool.FileSize(int64(m.MSpanSys)) | |||
sysStatus.MCacheInuse = tool.FileSize(int64(m.MCacheInuse)) | |||
sysStatus.MCacheSys = tool.FileSize(int64(m.MCacheSys)) | |||
sysStatus.BuckHashSys = tool.FileSize(int64(m.BuckHashSys)) | |||
sysStatus.GCSys = tool.FileSize(int64(m.GCSys)) | |||
sysStatus.OtherSys = tool.FileSize(int64(m.OtherSys)) | |||
sysStatus.NextGC = tool.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 { | |||
@@ -109,6 +109,44 @@ func (h *Handler) User(f userHandlerFunc) http.HandlerFunc { | |||
} | |||
} | |||
// Admin handles requests on /admin routes | |||
func (h *Handler) Admin(f userHandlerFunc) http.HandlerFunc { | |||
return func(w http.ResponseWriter, r *http.Request) { | |||
h.handleHTTPError(w, r, func() error { | |||
var status int | |||
start := time.Now() | |||
defer func() { | |||
if e := recover(); e != nil { | |||
log.Error("%s: %s", e, debug.Stack()) | |||
h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app, r)) | |||
status = http.StatusInternalServerError | |||
} | |||
log.Info(fmt.Sprintf("\"%s %s\" %d %s \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent())) | |||
}() | |||
u := getUserSession(h.app, r) | |||
if u == nil || !u.IsAdmin() { | |||
err := impart.HTTPError{http.StatusNotFound, ""} | |||
status = err.Status | |||
return err | |||
} | |||
err := f(h.app, u, w, r) | |||
if err == nil { | |||
status = http.StatusOK | |||
} else if err, ok := err.(impart.HTTPError); ok { | |||
status = err.Status | |||
} else { | |||
status = http.StatusInternalServerError | |||
} | |||
return err | |||
}()) | |||
} | |||
} | |||
// UserAPI handles requests made in the API by the authenticated user. | |||
// This provides user-friendly HTML pages and actions that work in the browser. | |||
func (h *Handler) UserAPI(f userHandlerFunc) http.HandlerFunc { | |||
@@ -115,6 +115,8 @@ func initRoutes(handler *Handler, r *mux.Router, cfg *config.Config, db *datasto | |||
} | |||
write.HandleFunc("/auth/login", handler.Web(webLogin, UserLevelNoneRequired)).Methods("POST") | |||
write.HandleFunc("/admin", handler.Admin(handleViewAdminDash)).Methods("GET") | |||
// Handle special pages first | |||
write.HandleFunc("/login", handler.Web(viewLogin, UserLevelNoneRequired)) | |||
@@ -0,0 +1,108 @@ | |||
{{define "admin"}} | |||
{{template "header" .}} | |||
<style type="text/css"> | |||
h2 {font-weight: normal;} | |||
ul.pagenav {list-style: none;} | |||
form {margin: 2em 0;} | |||
.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 tight"> | |||
<h2>Admin Dashboard</h2> | |||
{{if .Message}}<p>{{.Message}}</p>{{end}} | |||
<ul class="pagenav"> | |||
<li><a href="#monitor">Application monitor</a></li> | |||
</ul> | |||
<hr /> | |||
<h3><a name="monitor"></a>application monitor</h3> | |||
<div class="ui attached table segment"> | |||
<dl class="dl-horizontal admin-dl-horizontal"> | |||
<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}} |
@@ -7,7 +7,7 @@ h3 { font-weight: normal; } | |||
.section > *:not(input) { font-size: 0.86em; } | |||
</style> | |||
<div class="content-container snug regular"> | |||
<h2>{{if .IsLogOut}}Before you go...{{else}}Account Settings{{end}}</h2> | |||
<h2>{{if .IsLogOut}}Before you go...{{else}}Account Settings {{if .IsAdmin}}<a href="/admin">admin settings</a>{{end}}{{end}}</h2> | |||
{{if .Flashes}}<ul class="errors"> | |||
{{range .Flashes}}<li class="urgent">{{.}}</li>{{end}} | |||
</ul>{{end}} | |||
@@ -92,3 +92,8 @@ func (u User) Cookie() *User { | |||
return &u | |||
} | |||
func (u *User) IsAdmin() bool { | |||
// TODO: get this from database | |||
return u.ID == 1 | |||
} |