diff --git a/admin.go b/admin.go index 6d3cffd..c5e762b 100644 --- a/admin.go +++ b/admin.go @@ -3,6 +3,7 @@ package writefreely import ( "fmt" "github.com/gogits/gogs/pkg/tool" + "github.com/gorilla/mux" "github.com/writeas/impart" "github.com/writeas/web-core/auth" "net/http" @@ -62,16 +63,47 @@ func handleViewAdminDash(app *app, u *User, w http.ResponseWriter, r *http.Reque *UserPage Message string SysStatus systemStatus + + AboutPage, PrivacyPage string }{ - NewUserPage(app, r, u, "Admin", nil), - r.FormValue("m"), - sysStatus, + UserPage: NewUserPage(app, r, u, "Admin", nil), + Message: r.FormValue("m"), + SysStatus: sysStatus, + } + + 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 updateAppStats() { sysStatus.Uptime = tool.TimeSincePro(appStartTime) diff --git a/app.go b/app.go index 91f5cb1..0514f38 100644 --- a/app.go +++ b/app.go @@ -93,6 +93,42 @@ func handleViewHome(app *app, w http.ResponseWriter, r *http.Request) error { return renderPage(w, "landing.tmpl", p) } +func handleTemplatedPage(app *app, w http.ResponseWriter, r *http.Request, t *template.Template) error { + p := struct { + page.StaticPage + Content template.HTML + Updated string + }{ + StaticPage: pageForReq(app, r), + } + if r.URL.Path == "/about" || r.URL.Path == "/privacy" { + var c string + var updated *time.Time + var err error + + if r.URL.Path == "/about" { + c, err = getAboutPage(app) + } else { + c, updated, err = getPrivacyPage(app) + } + + if err != nil { + return err + } + p.Content = template.HTML(applyMarkdown([]byte(c))) + if updated != nil { + p.Updated = updated.Format("January 2, 2006") + } + } + + // Serve templated page + err := t.ExecuteTemplate(w, "base", p) + if err != nil { + log.Error("Unable to render page: %v", err) + } + return nil +} + func pageForReq(app *app, r *http.Request) page.StaticPage { p := page.StaticPage{ AppCfg: app.cfg.App, diff --git a/database.go b/database.go index b9888e6..0c79e10 100644 --- a/database.go +++ b/database.go @@ -91,6 +91,9 @@ type writestore interface { GetAPFollowers(c *Collection) (*[]RemoteUser, error) GetAPActorKeys(collectionID int64) ([]byte, []byte) + + GetDynamicContent(id string) (string, *time.Time, error) + UpdateDynamicContent(id, content string) error } type datastore struct { @@ -2105,6 +2108,28 @@ func (db *datastore) GetAPActorKeys(collectionID int64) ([]byte, []byte) { return pub, priv } +func (db *datastore) GetDynamicContent(id string) (string, *time.Time, error) { + var c string + var u *time.Time + err := db.QueryRow("SELECT content, updated FROM appcontent WHERE id = ?", id).Scan(&c, &u) + switch { + case err == sql.ErrNoRows: + return "", nil, nil + case err != nil: + log.Error("Couldn't SELECT FROM appcontent for id '%s': %v", id, err) + return "", nil, err + } + return c, u, nil +} + +func (db *datastore) UpdateDynamicContent(id, content string) error { + _, err := db.Exec("INSERT INTO appcontent (id, content, updated) VALUES (?, ?, NOW()) ON DUPLICATE KEY UPDATE content = ?, updated = NOW()", id, content, content) + if err != nil { + log.Error("Unable to INSERT appcontent for '%s': %v", id, err) + } + return err +} + func stringLogln(log *string, s string, v ...interface{}) { *log += fmt.Sprintf(s+"\n", v...) } diff --git a/less/admin.less b/less/admin.less new file mode 100644 index 0000000..4f6e8ac --- /dev/null +++ b/less/admin.less @@ -0,0 +1,4 @@ +.edit-page { + font-size: 1em; + min-height: 12em; +} diff --git a/less/app.less b/less/app.less index 7f32c1c..ec3472d 100644 --- a/less/app.less +++ b/less/app.less @@ -4,6 +4,7 @@ @import "pad-theme"; @import "post-temp"; @import "effects"; +@import "admin"; @import "pages/error"; @import "lib/elements"; @import "lib/material"; diff --git a/pages.go b/pages.go new file mode 100644 index 0000000..901faec --- /dev/null +++ b/pages.go @@ -0,0 +1,39 @@ +package writefreely + +import ( + "time" +) + +func getAboutPage(app *app) (string, error) { + c, _, err := app.db.GetDynamicContent("about") + if err != nil { + return "", err + } + if c == "" { + if app.cfg.App.Federation { + c = `_` + app.cfg.App.SiteName + `_ is an interconnected place for you to write and publish, powered by WriteFreely and ActivityPub.` + } else { + c = `_` + app.cfg.App.SiteName + `_ is a place for you to write and publish, powered by WriteFreely.` + } + } + return c, nil +} + +func getPrivacyPage(app *app) (string, *time.Time, error) { + c, updated, err := app.db.GetDynamicContent("privacy") + if err != nil { + return "", nil, err + } + if c == "" { + c = `[Write Freely](https://writefreely.org), the software that powers this site, is built to enforce your right to privacy by default. + +It retains as little data about you as possible, not even requiring an email address to sign up. However, if you _do_ give us your email address, it is stored encrypted in our database. We salt and hash your account's password. + +We store log files, or data about what happens on our servers. We also use cookies to keep you logged in to your account. + +Beyond this, it's important that you trust whoever runs **` + app.cfg.App.SiteName + `**. Software can only do so much to protect you -- your level of privacy protections will ultimately fall on the humans that run this particular service.` + defaultTime := time.Date(2018, 11, 8, 12, 0, 0, 0, time.Local) + updated = &defaultTime + } + return c, updated, nil +} diff --git a/pages/about.tmpl b/pages/about.tmpl index c816e5b..a47faf6 100644 --- a/pages/about.tmpl +++ b/pages/about.tmpl @@ -4,18 +4,7 @@

About {{.SiteName}}

- - -

- {{ if .Federation }} - {{.SiteName}} is an interconnected place for you to write and publish, powered by WriteFreely and ActivityPub. - {{ else }} - {{.SiteName}} is a place for you to write and publish, powered by WriteFreely. - {{ end }} -

- + {{.Content}}

About WriteFreely

WriteFreely is a self-hosted, decentralized blogging platform for publishing beautiful, simple blogs.

diff --git a/pages/privacy.tmpl b/pages/privacy.tmpl index 7d778b2..a32d028 100644 --- a/pages/privacy.tmpl +++ b/pages/privacy.tmpl @@ -2,10 +2,8 @@ {{end}} {{define "content"}}

Privacy Policy

-

Last updated November 8, 2018

-

Write Freely, the software that powers this site, is built to enforce your right to privacy by default.

-

It retains as little data about you as possible, not even requiring an email address to sign up. However, if you do give us your email address, it is stored encrypted in our database. We salt and hash your account's password.

-

We store log files, or data about what happens on our servers. We also use cookies to keep you logged in to your account.

-

Beyond this, it's important that you trust whoever runs {{.SiteName}}. Software can only do so much to protect you — your level of privacy protections will ultimately fall on the humans that run this particular service.

+

Last updated {{.Updated}}

+ + {{.Content}}
{{end}} diff --git a/posts.go b/posts.go index 46c44ce..205e2ad 100644 --- a/posts.go +++ b/posts.go @@ -258,12 +258,7 @@ func handleViewPost(app *app, w http.ResponseWriter, r *http.Request) error { // Display reserved page if that is requested resource if t, ok := pages[r.URL.Path[1:]+".tmpl"]; ok { - // Serve templated page - err := t.ExecuteTemplate(w, "base", pageForReq(app, r)) - if err != nil { - log.Error("Unable to render page: %v", err) - } - return nil + return handleTemplatedPage(app, w, r, t) } else if (strings.Contains(r.URL.Path, ".") && !isRaw && !isMarkdown) || r.URL.Path == "/robots.txt" || r.URL.Path == "/manifest.json" { // Serve static file shttp.ServeHTTP(w, r) diff --git a/routes.go b/routes.go index 28a1d3f..9b376bf 100644 --- a/routes.go +++ b/routes.go @@ -116,6 +116,7 @@ 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") + write.HandleFunc("/admin/update/{page}", handler.Admin(handleAdminUpdateSite)).Methods("POST") // Handle special pages first write.HandleFunc("/login", handler.Web(viewLogin, UserLevelNoneRequired)) diff --git a/schema.sql b/schema.sql index 7067a91..5cbe12d 100644 --- a/schema.sql +++ b/schema.sql @@ -21,6 +21,19 @@ CREATE TABLE IF NOT EXISTS `accesstokens` ( -- -------------------------------------------------------- +-- +-- Table structure for table `appcontent` +-- + +CREATE TABLE IF NOT EXISTS `appcontent` ( + `id` varchar(36) NOT NULL, + `content` mediumtext CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `updated` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=latin1; + +-- -------------------------------------------------------- + -- -- Table structure for table `collectionattributes` -- diff --git a/templates/user/admin.tmpl b/templates/user/admin.tmpl index cf6ac8f..228dfc3 100644 --- a/templates/user/admin.tmpl +++ b/templates/user/admin.tmpl @@ -4,7 +4,9 @@ -
-

Admin Dashboard

+ + +
+

Admin Dashboard

{{if .Message}}

{{.Message}}

{{end}}
-

application monitor

+ {{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>
+ +
+ +

Application

+
Server Uptime
@@ -103,6 +146,8 @@ form {margin: 2em 0;}
+ {{template "footer" .}} + {{template "body-end" .}} {{end}}