Support editing About and Privacy pages from Admin panel

This allows admin to edit these pages from the web, using Markdown. It
also dynamically loads information on those pages now, and makes loading
`pages` templates a little easier to find in the code / more explicit.

It requires this new schema change:

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;

This closes T533
This commit is contained in:
Matt Baer 2018-11-18 21:58:50 -05:00
parent 7d87aad55a
commit bdc4f270f8
12 changed files with 208 additions and 30 deletions

View File

@ -3,6 +3,7 @@ package writefreely
import ( import (
"fmt" "fmt"
"github.com/gogits/gogs/pkg/tool" "github.com/gogits/gogs/pkg/tool"
"github.com/gorilla/mux"
"github.com/writeas/impart" "github.com/writeas/impart"
"github.com/writeas/web-core/auth" "github.com/writeas/web-core/auth"
"net/http" "net/http"
@ -62,16 +63,47 @@ func handleViewAdminDash(app *app, u *User, w http.ResponseWriter, r *http.Reque
*UserPage *UserPage
Message string Message string
SysStatus systemStatus SysStatus systemStatus
AboutPage, PrivacyPage string
}{ }{
NewUserPage(app, r, u, "Admin", nil), UserPage: NewUserPage(app, r, u, "Admin", nil),
r.FormValue("m"), Message: r.FormValue("m"),
sysStatus, 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) showUserPage(w, "admin", p)
return nil 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() { func updateAppStats() {
sysStatus.Uptime = tool.TimeSincePro(appStartTime) sysStatus.Uptime = tool.TimeSincePro(appStartTime)

36
app.go
View File

@ -93,6 +93,42 @@ func handleViewHome(app *app, w http.ResponseWriter, r *http.Request) error {
return renderPage(w, "landing.tmpl", p) 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 { func pageForReq(app *app, r *http.Request) page.StaticPage {
p := page.StaticPage{ p := page.StaticPage{
AppCfg: app.cfg.App, AppCfg: app.cfg.App,

View File

@ -91,6 +91,9 @@ type writestore interface {
GetAPFollowers(c *Collection) (*[]RemoteUser, error) GetAPFollowers(c *Collection) (*[]RemoteUser, error)
GetAPActorKeys(collectionID int64) ([]byte, []byte) GetAPActorKeys(collectionID int64) ([]byte, []byte)
GetDynamicContent(id string) (string, *time.Time, error)
UpdateDynamicContent(id, content string) error
} }
type datastore struct { type datastore struct {
@ -2105,6 +2108,28 @@ func (db *datastore) GetAPActorKeys(collectionID int64) ([]byte, []byte) {
return pub, priv 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{}) { func stringLogln(log *string, s string, v ...interface{}) {
*log += fmt.Sprintf(s+"\n", v...) *log += fmt.Sprintf(s+"\n", v...)
} }

4
less/admin.less Normal file
View File

@ -0,0 +1,4 @@
.edit-page {
font-size: 1em;
min-height: 12em;
}

View File

@ -4,6 +4,7 @@
@import "pad-theme"; @import "pad-theme";
@import "post-temp"; @import "post-temp";
@import "effects"; @import "effects";
@import "admin";
@import "pages/error"; @import "pages/error";
@import "lib/elements"; @import "lib/elements";
@import "lib/material"; @import "lib/material";

39
pages.go Normal file
View File

@ -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
}

View File

@ -4,18 +4,7 @@
<div class="content-container snug"> <div class="content-container snug">
<h1>About {{.SiteName}}</h1> <h1>About {{.SiteName}}</h1>
<!-- {{.Content}}
Feel free to edit this section. Describe what your instance is about!
-->
<p>
{{ if .Federation }}
<em>{{.SiteName}}</em> is an interconnected place for you to write and publish, powered by WriteFreely and ActivityPub.
{{ else }}
<em>{{.SiteName}}</em> is a place for you to write and publish, powered by WriteFreely.
{{ end }}
</p>
<h2 style="margin-top:2em">About WriteFreely</h2> <h2 style="margin-top:2em">About WriteFreely</h2>
<p><a href="https://writefreely.org">WriteFreely</a> is a self-hosted, decentralized blogging platform for publishing beautiful, simple blogs.</p> <p><a href="https://writefreely.org">WriteFreely</a> is a self-hosted, decentralized blogging platform for publishing beautiful, simple blogs.</p>

View File

@ -2,10 +2,8 @@
{{end}} {{end}}
{{define "content"}}<div class="content-container snug"> {{define "content"}}<div class="content-container snug">
<h1>Privacy Policy</h1> <h1>Privacy Policy</h1>
<p style="font-style:italic">Last updated November 8, 2018</p> <p style="font-style:italic">Last updated {{.Updated}}</p>
<p class="statement"><a href="https://writefreely.org">Write Freely</a>, the software that powers this site, is built to enforce your right to privacy by default.</p>
<p>It retains as little data about you as possible, not even requiring an email address to sign up. However, if you <em>do</em> give us your email address, it is stored encrypted in our database. We salt and hash your account's password.</p> {{.Content}}
<p>We store log files, or data about what happens on our servers. We also use cookies to keep you logged in to your account.</p>
<p>Beyond this, it's important that you trust whoever runs <strong>{{.SiteName}}</strong>. Software can only do so much to protect you &mdash; your level of privacy protections will ultimately fall on the humans that run this particular service.</p>
</div> </div>
{{end}} {{end}}

View File

@ -258,12 +258,7 @@ func handleViewPost(app *app, w http.ResponseWriter, r *http.Request) error {
// Display reserved page if that is requested resource // Display reserved page if that is requested resource
if t, ok := pages[r.URL.Path[1:]+".tmpl"]; ok { if t, ok := pages[r.URL.Path[1:]+".tmpl"]; ok {
// Serve templated page return handleTemplatedPage(app, w, r, t)
err := t.ExecuteTemplate(w, "base", pageForReq(app, r))
if err != nil {
log.Error("Unable to render page: %v", err)
}
return nil
} else if (strings.Contains(r.URL.Path, ".") && !isRaw && !isMarkdown) || r.URL.Path == "/robots.txt" || r.URL.Path == "/manifest.json" { } else if (strings.Contains(r.URL.Path, ".") && !isRaw && !isMarkdown) || r.URL.Path == "/robots.txt" || r.URL.Path == "/manifest.json" {
// Serve static file // Serve static file
shttp.ServeHTTP(w, r) shttp.ServeHTTP(w, r)

View File

@ -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("/auth/login", handler.Web(webLogin, UserLevelNoneRequired)).Methods("POST")
write.HandleFunc("/admin", handler.Admin(handleViewAdminDash)).Methods("GET") write.HandleFunc("/admin", handler.Admin(handleViewAdminDash)).Methods("GET")
write.HandleFunc("/admin/update/{page}", handler.Admin(handleAdminUpdateSite)).Methods("POST")
// Handle special pages first // Handle special pages first
write.HandleFunc("/login", handler.Web(viewLogin, UserLevelNoneRequired)) write.HandleFunc("/login", handler.Web(viewLogin, UserLevelNoneRequired))

View File

@ -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` -- Table structure for table `collectionattributes`
-- --

View File

@ -4,7 +4,9 @@
<style type="text/css"> <style type="text/css">
h2 {font-weight: normal;} h2 {font-weight: normal;}
ul.pagenav {list-style: none;} ul.pagenav {list-style: none;}
form {margin: 2em 0;} form {
margin: 0 0 2em;
}
.ui.divider:not(.vertical):not(.horizontal) { .ui.divider:not(.vertical):not(.horizontal) {
border-top: 1px solid rgba(34,36,38,.15); border-top: 1px solid rgba(34,36,38,.15);
border-bottom: 1px solid rgba(255,255,255,.1); border-bottom: 1px solid rgba(255,255,255,.1);
@ -26,18 +28,59 @@ form {margin: 2em 0;}
} }
</style> </style>
<div class="content-container tight"> <script>
<h2>Admin Dashboard</h2> function savePage(el) {
var $btn = el.querySelector('input[type=submit]');
$btn.value = 'Saving...';
$btn.disabled = true;
}
</script>
<div class="content-container snug">
<h1>Admin Dashboard</h1>
{{if .Message}}<p>{{.Message}}</p>{{end}} {{if .Message}}<p>{{.Message}}</p>{{end}}
<ul class="pagenav"> <ul class="pagenav">
{{if not .SingleUser}}
<li><a href="#page-about">Edit About page</a></li>
<li><a href="#page-privacy">Edit Privacy page</a></li>
{{end}}
<li><a href="#reset-pass">Reset user password</a></li>
<li><a href="#monitor">Application monitor</a></li> <li><a href="#monitor">Application monitor</a></li>
</ul> </ul>
<hr /> <hr />
<h3><a name="monitor"></a>application monitor</h3> {{if not .SingleUser}}
<h2>Site</h2>
<h3 id="page-about">About page</h3>
<p>Describe what your instance is <a href="/privacy">about</a>. <em>Accepts Markdown</em>.</p>
<form method="post" action="/admin/update/about" onsubmit="savePage(this)">
<textarea id="about-editor" class="section codable norm edit-page" name="content">{{.AboutPage}}</textarea>
<input type="submit" value="Save" />
</form>
<h3 id="page-privacy">Privacy page</h3>
<p>Outline your <a href="/privacy">privacy policy</a>. <em>Accepts Markdown</em>.</p>
<form method="post" action="/admin/update/privacy" onsubmit="savePage(this)">
<textarea id="privacy-editor" class="section codable norm edit-page" name="content">{{.PrivacyPage}}</textarea>
<input type="submit" value="Save" />
</form>
<hr />
{{end}}
<h2>Users</h2>
<h3><a name="reset-pass"></a>reset password</h3>
<pre><code>writefreely --reset-pass &lt;username&gt;</code></pre>
<hr />
<h2><a name="monitor"></a>Application</h2>
<div class="ui attached table segment"> <div class="ui attached table segment">
<dl class="dl-horizontal admin-dl-horizontal"> <dl class="dl-horizontal admin-dl-horizontal">
<dt>Server Uptime</dt> <dt>Server Uptime</dt>
@ -103,6 +146,8 @@ form {margin: 2em 0;}
</dl> </dl>
</div> </div>
</div> </div>
{{template "footer" .}} {{template "footer" .}}
{{template "body-end" .}} {{template "body-end" .}}
{{end}} {{end}}