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:
parent
7d87aad55a
commit
bdc4f270f8
38
admin.go
38
admin.go
|
@ -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
36
app.go
|
@ -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,
|
||||||
|
|
25
database.go
25
database.go
|
@ -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...)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
.edit-page {
|
||||||
|
font-size: 1em;
|
||||||
|
min-height: 12em;
|
||||||
|
}
|
|
@ -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";
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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>
|
||||||
|
|
|
@ -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 — your level of privacy protections will ultimately fall on the humans that run this particular service.</p>
|
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
7
posts.go
7
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
|
// 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)
|
||||||
|
|
|
@ -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))
|
||||||
|
|
13
schema.sql
13
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`
|
-- Table structure for table `collectionattributes`
|
||||||
--
|
--
|
||||||
|
|
|
@ -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 <username></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}}
|
||||||
|
|
Loading…
Reference in New Issue