diff --git a/admin.go b/admin.go index c5e762b..7785a5f 100644 --- a/admin.go +++ b/admin.go @@ -1,156 +1,186 @@ package writefreely import ( "fmt" "github.com/gogits/gogs/pkg/tool" "github.com/gorilla/mux" "github.com/writeas/impart" "github.com/writeas/web-core/auth" + "github.com/writeas/writefreely/config" "net/http" "runtime" + "strconv" "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 + Config config.AppCfg + + Message, ConfigMessage string AboutPage, PrivacyPage string }{ UserPage: NewUserPage(app, r, u, "Admin", nil), - Message: r.FormValue("m"), SysStatus: sysStatus, + Config: app.cfg.App, + + Message: r.FormValue("m"), + ConfigMessage: r.FormValue("cm"), } var err error p.AboutPage, err = getAboutPage(app) if err != nil { return err } p.PrivacyPage, _, err = getPrivacyPage(app) if err != nil { return err } showUserPage(w, "admin", 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" { return impart.HTTPError{http.StatusNotFound, "No such page."} } // Update page m := "" err := app.db.UpdateDynamicContent(id, r.FormValue("content")) if err != nil { m = "?m=" + err.Error() } return impart.HTTPError{http.StatusFound, "/admin" + m + "#page-" + id} } +func handleAdminUpdateConfig(app *app, u *User, w http.ResponseWriter, r *http.Request) error { + app.cfg.App.SiteName = r.FormValue("site_name") + app.cfg.App.OpenRegistration = r.FormValue("open_registration") == "on" + mul, err := strconv.Atoi(r.FormValue("min_username_len")) + if err == nil { + app.cfg.App.MinUsernameLen = mul + } + mb, err := strconv.Atoi(r.FormValue("max_blogs")) + if err == nil { + app.cfg.App.MaxBlogs = mb + } + app.cfg.App.Federation = r.FormValue("federation") == "on" + app.cfg.App.PublicStats = r.FormValue("public_stats") == "on" + app.cfg.App.Private = r.FormValue("private") == "on" + + m := "?cm=Configuration+saved." + err = config.Save(app.cfg) + if err != nil { + m = "?cm=" + err.Error() + } + return impart.HTTPError{http.StatusFound, "/admin" + m + "#config"} +} + 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 { 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/routes.go b/routes.go index 9b376bf..df57373 100644 --- a/routes.go +++ b/routes.go @@ -1,159 +1,160 @@ package writefreely import ( "github.com/gorilla/mux" "github.com/writeas/go-nodeinfo" "github.com/writeas/go-webfinger" "github.com/writeas/web-core/log" "github.com/writeas/writefreely/config" "net/http" "strings" ) func initRoutes(handler *Handler, r *mux.Router, cfg *config.Config, db *datastore) { hostSubroute := cfg.App.Host[strings.Index(cfg.App.Host, "://")+3:] if cfg.App.SingleUser { hostSubroute = "{domain}" } else { if strings.HasPrefix(hostSubroute, "localhost") { hostSubroute = "localhost" } } if 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{db, cfg}) wf.NoTLSHandler = nil // Federation endpoints // host-meta write.HandleFunc("/.well-known/host-meta", handler.Web(handleViewHostMeta, UserLevelOptional)) // webfinger write.HandleFunc(webfinger.WebFingerPath, handler.LogHandlerFunc(http.HandlerFunc(wf.Webfinger))) // nodeinfo niCfg := nodeInfoConfig(db, cfg) ni := nodeinfo.NewService(*niCfg, nodeInfoResolver{cfg, db}) write.HandleFunc(nodeinfo.NodeInfoPath, handler.LogHandlerFunc(http.HandlerFunc(ni.NodeInfoDiscover))) write.HandleFunc(niCfg.InfoURL, handler.LogHandlerFunc(http.HandlerFunc(ni.NodeInfo))) // Set up dyamic page handlers // Handle auth auth := write.PathPrefix("/api/auth/").Subrouter() if 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("/settings", handler.User(viewSettings)).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") // Sign up validation write.HandleFunc("/api/alias", handler.All(handleUsernameCheck)).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.All(fetchCollection)).Methods("GET") apiColls.HandleFunc("/{alias:[0-9a-zA-Z\\-]+}", handler.All(existingCollection)).Methods("POST", "DELETE") apiColls.HandleFunc("/{alias}/posts", handler.All(fetchCollectionPosts)).Methods("GET") apiColls.HandleFunc("/{alias}/posts", handler.All(newPost)).Methods("POST") apiColls.HandleFunc("/{alias}/posts/{post}", handler.All(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.All(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.All(handleFetchCollectionOutbox)).Methods("GET") apiColls.HandleFunc("/{alias}/following", handler.All(handleFetchCollectionFollowing)).Methods("GET") apiColls.HandleFunc("/{alias}/followers", handler.All(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.All(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.All(fetchPostProperty)).Methods("GET") posts.HandleFunc("/claim", handler.All(addPost)).Methods("POST") posts.HandleFunc("/disperse", handler.All(dispersePost)).Methods("POST") if cfg.App.OpenRegistration { 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/update/config", handler.Admin(handleAdminUpdateConfig)).Methods("POST") write.HandleFunc("/admin/update/{page}", handler.Admin(handleAdminUpdateSite)).Methods("POST") // Handle special pages first write.HandleFunc("/login", handler.Web(viewLogin, UserLevelNoneRequired)) draftEditPrefix := "" if cfg.App.SingleUser { draftEditPrefix = "/d" write.HandleFunc("/me/new", handler.Web(handleViewPad, UserLevelOptional)).Methods("GET") } else { write.HandleFunc("/new", handler.Web(handleViewPad, UserLevelOptional)).Methods("GET") } // All the existing stuff write.HandleFunc(draftEditPrefix+"/{action}/edit", handler.Web(handleViewPad, UserLevelOptional)).Methods("GET") write.HandleFunc(draftEditPrefix+"/{action}/meta", handler.Web(handleViewMeta, UserLevelOptional)).Methods("GET") // Collections if cfg.App.SingleUser { RouteCollections(handler, write.PathPrefix("/").Subrouter()) } else { write.HandleFunc("/{prefix:[@~$!\\-+]}{collection}", handler.Web(handleViewCollection, UserLevelOptional)) write.HandleFunc("/{collection}/", handler.Web(handleViewCollection, UserLevelOptional)) RouteCollections(handler, write.PathPrefix("/{prefix:[@~$!\\-+]?}{collection}").Subrouter()) // Posts } write.HandleFunc(draftEditPrefix+"/{post}", handler.Web(handleViewPost, UserLevelOptional)) write.HandleFunc("/", handler.Web(handleViewHome, UserLevelOptional)) } func RouteCollections(handler *Handler, r *mux.Router) { r.HandleFunc("/page/{page:[0-9]+}", handler.Web(handleViewCollection, UserLevelOptional)) r.HandleFunc("/tag:{tag}", handler.Web(handleViewCollectionTag, UserLevelOptional)) r.HandleFunc("/tag:{tag}/feed/", handler.Web(ViewFeed, UserLevelOptional)) r.HandleFunc("/tags/{tag}", handler.Web(handleViewCollectionTag, UserLevelOptional)) r.HandleFunc("/sitemap.xml", handler.All(handleViewSitemap)) r.HandleFunc("/feed/", handler.All(ViewFeed)) r.HandleFunc("/{slug}", handler.Web(viewCollectionPost, UserLevelOptional)) r.HandleFunc("/{slug}/edit", handler.Web(handleViewPad, UserLevelUser)) r.HandleFunc("/{slug}/edit/meta", handler.Web(handleViewMeta, UserLevelUser)) r.HandleFunc("/{slug}/", handler.Web(handleCollectionPostRedirect, UserLevelOptional)).Methods("GET") } diff --git a/templates/user/admin.tmpl b/templates/user/admin.tmpl index bdfbd71..c855fa4 100644 --- a/templates/user/admin.tmpl +++ b/templates/user/admin.tmpl @@ -1,153 +1,189 @@ {{define "admin"}} {{template "header" .}}

Admin Dashboard

{{if .Message}}

{{.Message}}

{{end}}
{{if not .SingleUser}}

Site

About page

Describe what your instance is about. Accepts Markdown.

Privacy page

Outline your privacy policy. Accepts Markdown.


{{end}}

Users

reset password

writefreely --reset-pass <username>

+

App Configuration

+ + {{if .ConfigMessage}}

{{.ConfigMessage}}

{{end}} + +
+
+
+
Site Name
+
+
Host
+
{{.Config.Host}}
+
User Mode
+
{{if .Config.SingleUser}}Single user{{else}}Multiple users{{end}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ +
+

Application

Server Uptime
{{.SysStatus.Uptime}}
Current Goroutines
{{.SysStatus.NumGoroutine}}
Current memory usage
{{.SysStatus.MemAllocated}}
Total mem allocated
{{.SysStatus.MemTotal}}
Memory obtained
{{.SysStatus.MemSys}}
Pointer lookup times
{{.SysStatus.Lookups}}
Memory allocate times
{{.SysStatus.MemMallocs}}
Memory free times
{{.SysStatus.MemFrees}}
Current heap usage
{{.SysStatus.HeapAlloc}}
Heap memory obtained
{{.SysStatus.HeapSys}}
Heap memory idle
{{.SysStatus.HeapIdle}}
Heap memory in use
{{.SysStatus.HeapInuse}}
Heap memory released
{{.SysStatus.HeapReleased}}
Heap objects
{{.SysStatus.HeapObjects}}
Bootstrap stack usage
{{.SysStatus.StackInuse}}
Stack memory obtained
{{.SysStatus.StackSys}}
MSpan structures in use
{{.SysStatus.MSpanInuse}}
MSpan structures obtained
{{.SysStatus.HeapSys}}
MCache structures in use
{{.SysStatus.MCacheInuse}}
MCache structures obtained
{{.SysStatus.MCacheSys}}
Profiling bucket hash table obtained
{{.SysStatus.BuckHashSys}}
GC metadata obtained
{{.SysStatus.GCSys}}
Other system allocation obtained
{{.SysStatus.OtherSys}}
Next GC recycle
{{.SysStatus.NextGC}}
Since last GC
{{.SysStatus.LastGC}}
Total GC pause
{{.SysStatus.PauseTotalNs}}
Last GC pause
{{.SysStatus.PauseNs}}
GC times
{{.SysStatus.NumGC}}
{{template "footer" .}} {{template "body-end" .}} {{end}}