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 T533tags/v0.3.0^0
@@ -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) | |||
@@ -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, | |||
@@ -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...) | |||
} | |||
@@ -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"; |
@@ -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}} |
@@ -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)) | |||
@@ -22,6 +22,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}} |