mirror of
https://github.com/writefreely/writefreely
synced 2024-11-10 11:24:13 +00:00
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
12 changed files with 208 additions and 30 deletions
38
admin.go
38
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)
|
||||
|
||||
|
|
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)
|
||||
}
|
||||
|
||||
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,
|
||||
|
|
25
database.go
25
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...)
|
||||
}
|
||||
|
|
4
less/admin.less
Normal file
4
less/admin.less
Normal file
|
@ -0,0 +1,4 @@
|
|||
.edit-page {
|
||||
font-size: 1em;
|
||||
min-height: 12em;
|
||||
}
|
|
@ -4,6 +4,7 @@
|
|||
@import "pad-theme";
|
||||
@import "post-temp";
|
||||
@import "effects";
|
||||
@import "admin";
|
||||
@import "pages/error";
|
||||
@import "lib/elements";
|
||||
@import "lib/material";
|
||||
|
|
39
pages.go
Normal file
39
pages.go
Normal 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
|
||||
}
|
|
@ -4,18 +4,7 @@
|
|||
<div class="content-container snug">
|
||||
<h1>About {{.SiteName}}</h1>
|
||||
|
||||
<!--
|
||||
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>
|
||||
|
||||
{{.Content}}
|
||||
|
||||
<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>
|
||||
|
|
|
@ -2,10 +2,8 @@
|
|||
{{end}}
|
||||
{{define "content"}}<div class="content-container snug">
|
||||
<h1>Privacy Policy</h1>
|
||||
<p style="font-style:italic">Last updated November 8, 2018</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>
|
||||
<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>
|
||||
<p style="font-style:italic">Last updated {{.Updated}}</p>
|
||||
|
||||
{{.Content}}
|
||||
</div>
|
||||
{{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
|
||||
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)
|
||||
|
|
|
@ -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))
|
||||
|
|
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`
|
||||
--
|
||||
|
|
|
@ -4,7 +4,9 @@
|
|||
<style type="text/css">
|
||||
h2 {font-weight: normal;}
|
||||
ul.pagenav {list-style: none;}
|
||||
form {margin: 2em 0;}
|
||||
form {
|
||||
margin: 0 0 2em;
|
||||
}
|
||||
.ui.divider:not(.vertical):not(.horizontal) {
|
||||
border-top: 1px solid rgba(34,36,38,.15);
|
||||
border-bottom: 1px solid rgba(255,255,255,.1);
|
||||
|
@ -26,18 +28,59 @@ form {margin: 2em 0;}
|
|||
}
|
||||
</style>
|
||||
|
||||
<div class="content-container tight">
|
||||
<h2>Admin Dashboard</h2>
|
||||
<script>
|
||||
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}}
|
||||
|
||||
<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>
|
||||
</ul>
|
||||
|
||||
<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">
|
||||
<dl class="dl-horizontal admin-dl-horizontal">
|
||||
<dt>Server Uptime</dt>
|
||||
|
@ -103,6 +146,8 @@ form {margin: 2em 0;}
|
|||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{template "footer" .}}
|
||||
|
||||
{{template "body-end" .}}
|
||||
{{end}}
|
||||
|
|
Loading…
Reference in a new issue