mirror of
https://github.com/writefreely/writefreely
synced 2024-12-01 04:49:09 +00:00
Merge branch 'develop' into T661-disable-accounts
This commit is contained in:
commit
53586d9cb8
23 changed files with 150 additions and 64 deletions
10
README.md
10
README.md
|
@ -47,15 +47,15 @@ It's designed to be flexible and share your writing widely, so it's built around
|
||||||
|
|
||||||
## Hosting
|
## Hosting
|
||||||
|
|
||||||
We offer two kinds of hosting services that make WriteFreely deployment painless: [Write.as](https://write.as) for individuals, and [WriteFreely.host](https://writefreely.host) for communities. Besides saving you time, as a customer you directly help fund WriteFreely development.
|
We offer two kinds of hosting services that make WriteFreely deployment painless: [Write.as Pro](https://write.as/pro) for individuals, and [Write.as for Teams](https://write.as/for/teams) for businesses. Besides saving you time and effort, both services directly fund WriteFreely development and ensure the long-term sustainability of our open source work.
|
||||||
|
|
||||||
### [![Write.as](https://write.as/img/writeas-wf-readme.png)](https://write.as/)
|
### [![Write.as Pro](https://writefreely.org/img/writeas-pro-readme.png)](https://write.as/pro)
|
||||||
|
|
||||||
Start a personal blog on [Write.as](https://write.as), our flagship instance. Built to eliminate setup friction and preserve your privacy, Write.as helps you start a blog in seconds. It supports custom domains (with SSL) and multiple blogs / pen names per account. [Read more here](https://write.as/pricing).
|
Start a personal blog on [Write.as](https://write.as), our flagship instance. Built to eliminate setup friction and preserve your privacy, Write.as helps you start a blog in seconds. It supports custom domains (with SSL) and multiple blogs / pen names per account. [Read more here](https://write.as/pro).
|
||||||
|
|
||||||
### [![WriteFreely.host](https://writefreely.host/img/wfhost-wf-readme.png)](https://writefreely.host)
|
### [![Write.as for Teams](https://writefreely.org/img/writeas-for-teams-readme.png)](https://write.as/for/teams)
|
||||||
|
|
||||||
[WriteFreely.host](https://writefreely.host) makes it easy to start a close-knit community — to share knowledge, complement your Mastodon instance, or publish updates in your organization. We take care of the hosting, upgrades, backups, and maintenance so you can focus on writing.
|
[Write.as for Teams](https://write.as/for/teams) gives your organization, business, or [open source project](https://write.as/for/open-source) a clutter-free space to share updates or proposals and build your collective knowledge. We take care of hosting, upgrades, backups, and maintenance so your team can focus on writing.
|
||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
|
|
18
account.go
18
account.go
|
@ -85,7 +85,7 @@ func apiSignup(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func signup(app *App, w http.ResponseWriter, r *http.Request) (*AuthUser, error) {
|
func signup(app *App, w http.ResponseWriter, r *http.Request) (*AuthUser, error) {
|
||||||
reqJSON := IsJSON(r.Header.Get("Content-Type"))
|
reqJSON := IsJSON(r)
|
||||||
|
|
||||||
// Get params
|
// Get params
|
||||||
var ur userRegistration
|
var ur userRegistration
|
||||||
|
@ -120,7 +120,7 @@ func signup(app *App, w http.ResponseWriter, r *http.Request) (*AuthUser, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func signupWithRegistration(app *App, signup userRegistration, w http.ResponseWriter, r *http.Request) (*AuthUser, error) {
|
func signupWithRegistration(app *App, signup userRegistration, w http.ResponseWriter, r *http.Request) (*AuthUser, error) {
|
||||||
reqJSON := IsJSON(r.Header.Get("Content-Type"))
|
reqJSON := IsJSON(r)
|
||||||
|
|
||||||
// Validate required params (alias)
|
// Validate required params (alias)
|
||||||
if signup.Alias == "" {
|
if signup.Alias == "" {
|
||||||
|
@ -377,7 +377,7 @@ func webLogin(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||||
var loginAttemptUsers = sync.Map{}
|
var loginAttemptUsers = sync.Map{}
|
||||||
|
|
||||||
func login(app *App, w http.ResponseWriter, r *http.Request) error {
|
func login(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||||
reqJSON := IsJSON(r.Header.Get("Content-Type"))
|
reqJSON := IsJSON(r)
|
||||||
oneTimeToken := r.FormValue("with")
|
oneTimeToken := r.FormValue("with")
|
||||||
verbose := r.FormValue("all") == "true" || r.FormValue("verbose") == "1" || r.FormValue("verbose") == "true" || (reqJSON && oneTimeToken != "")
|
verbose := r.FormValue("all") == "true" || r.FormValue("verbose") == "1" || r.FormValue("verbose") == "true" || (reqJSON && oneTimeToken != "")
|
||||||
|
|
||||||
|
@ -580,7 +580,7 @@ func viewExportOptions(app *App, u *User, w http.ResponseWriter, r *http.Request
|
||||||
func viewExportPosts(app *App, w http.ResponseWriter, r *http.Request) ([]byte, string, error) {
|
func viewExportPosts(app *App, w http.ResponseWriter, r *http.Request) ([]byte, string, error) {
|
||||||
var filename string
|
var filename string
|
||||||
var u = &User{}
|
var u = &User{}
|
||||||
reqJSON := IsJSON(r.Header.Get("Content-Type"))
|
reqJSON := IsJSON(r)
|
||||||
if reqJSON {
|
if reqJSON {
|
||||||
// Use given Authorization header
|
// Use given Authorization header
|
||||||
accessToken := r.Header.Get("Authorization")
|
accessToken := r.Header.Get("Authorization")
|
||||||
|
@ -625,7 +625,7 @@ func viewExportPosts(app *App, w http.ResponseWriter, r *http.Request) ([]byte,
|
||||||
|
|
||||||
// Export as CSV
|
// Export as CSV
|
||||||
if strings.HasSuffix(r.URL.Path, ".csv") {
|
if strings.HasSuffix(r.URL.Path, ".csv") {
|
||||||
data = exportPostsCSV(u, posts)
|
data = exportPostsCSV(app.cfg.App.Host, u, posts)
|
||||||
return data, filename, err
|
return data, filename, err
|
||||||
}
|
}
|
||||||
if strings.HasSuffix(r.URL.Path, ".zip") {
|
if strings.HasSuffix(r.URL.Path, ".zip") {
|
||||||
|
@ -662,7 +662,7 @@ func viewExportFull(app *App, w http.ResponseWriter, r *http.Request) ([]byte, s
|
||||||
}
|
}
|
||||||
|
|
||||||
func viewMeAPI(app *App, w http.ResponseWriter, r *http.Request) error {
|
func viewMeAPI(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||||
reqJSON := IsJSON(r.Header.Get("Content-Type"))
|
reqJSON := IsJSON(r)
|
||||||
uObj := struct {
|
uObj := struct {
|
||||||
ID int64 `json:"id,omitempty"`
|
ID int64 `json:"id,omitempty"`
|
||||||
Username string `json:"username,omitempty"`
|
Username string `json:"username,omitempty"`
|
||||||
|
@ -686,7 +686,7 @@ func viewMeAPI(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func viewMyPostsAPI(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
|
func viewMyPostsAPI(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||||
reqJSON := IsJSON(r.Header.Get("Content-Type"))
|
reqJSON := IsJSON(r)
|
||||||
if !reqJSON {
|
if !reqJSON {
|
||||||
return ErrBadRequestedType
|
return ErrBadRequestedType
|
||||||
}
|
}
|
||||||
|
@ -717,7 +717,7 @@ func viewMyPostsAPI(app *App, u *User, w http.ResponseWriter, r *http.Request) e
|
||||||
}
|
}
|
||||||
|
|
||||||
func viewMyCollectionsAPI(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
|
func viewMyCollectionsAPI(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||||
reqJSON := IsJSON(r.Header.Get("Content-Type"))
|
reqJSON := IsJSON(r)
|
||||||
if !reqJSON {
|
if !reqJSON {
|
||||||
return ErrBadRequestedType
|
return ErrBadRequestedType
|
||||||
}
|
}
|
||||||
|
@ -842,7 +842,7 @@ func viewEditCollection(app *App, u *User, w http.ResponseWriter, r *http.Reques
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateSettings(app *App, w http.ResponseWriter, r *http.Request) error {
|
func updateSettings(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||||
reqJSON := IsJSON(r.Header.Get("Content-Type"))
|
reqJSON := IsJSON(r)
|
||||||
|
|
||||||
var s userSettings
|
var s userSettings
|
||||||
var u *User
|
var u *User
|
||||||
|
|
|
@ -415,12 +415,12 @@ func handleFetchCollectionInbox(app *App, w http.ResponseWriter, r *http.Request
|
||||||
// Add follower locally, since it wasn't found before
|
// Add follower locally, since it wasn't found before
|
||||||
res, err := t.Exec("INSERT INTO remoteusers (actor_id, inbox, shared_inbox) VALUES (?, ?, ?)", fullActor.ID, fullActor.Inbox, fullActor.Endpoints.SharedInbox)
|
res, err := t.Exec("INSERT INTO remoteusers (actor_id, inbox, shared_inbox) VALUES (?, ?, ?)", fullActor.ID, fullActor.Inbox, fullActor.Endpoints.SharedInbox)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !app.db.isDuplicateKeyErr(err) {
|
// if duplicate key, res will be nil and panic on
|
||||||
|
// res.LastInsertId below
|
||||||
t.Rollback()
|
t.Rollback()
|
||||||
log.Error("Couldn't add new remoteuser in DB: %v\n", err)
|
log.Error("Couldn't add new remoteuser in DB: %v\n", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
followerID, err = res.LastInsertId()
|
followerID, err = res.LastInsertId()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
45
admin.go
45
admin.go
|
@ -16,12 +16,14 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/writeas/impart"
|
"github.com/writeas/impart"
|
||||||
"github.com/writeas/web-core/auth"
|
"github.com/writeas/web-core/auth"
|
||||||
"github.com/writeas/web-core/log"
|
"github.com/writeas/web-core/log"
|
||||||
|
"github.com/writeas/web-core/passgen"
|
||||||
"github.com/writeas/writefreely/appstats"
|
"github.com/writeas/writefreely/appstats"
|
||||||
"github.com/writeas/writefreely/config"
|
"github.com/writeas/writefreely/config"
|
||||||
)
|
)
|
||||||
|
@ -173,8 +175,9 @@ func handleViewAdminUser(app *App, u *User, w http.ResponseWriter, r *http.Reque
|
||||||
User *User
|
User *User
|
||||||
Colls []inspectedCollection
|
Colls []inspectedCollection
|
||||||
LastPost string
|
LastPost string
|
||||||
|
NewPassword string
|
||||||
TotalPosts int64
|
TotalPosts int64
|
||||||
|
ClearEmail string
|
||||||
}{
|
}{
|
||||||
Config: app.cfg.App,
|
Config: app.cfg.App,
|
||||||
Message: r.FormValue("m"),
|
Message: r.FormValue("m"),
|
||||||
|
@ -186,6 +189,14 @@ func handleViewAdminUser(app *App, u *User, w http.ResponseWriter, r *http.Reque
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get user: %v", err)}
|
return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get user: %v", err)}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
flashes, _ := getSessionFlashes(app, w, r, nil)
|
||||||
|
for _, flash := range flashes {
|
||||||
|
if strings.HasPrefix(flash, "SUCCESS: ") {
|
||||||
|
p.NewPassword = strings.TrimPrefix(flash, "SUCCESS: ")
|
||||||
|
p.ClearEmail = p.User.EmailClear(app.keys)
|
||||||
|
}
|
||||||
|
}
|
||||||
p.UserPage = NewUserPage(app, r, u, p.User.Username, nil)
|
p.UserPage = NewUserPage(app, r, u, p.User.Username, nil)
|
||||||
p.TotalPosts = app.db.GetUserPostsCount(p.User.ID)
|
p.TotalPosts = app.db.GetUserPostsCount(p.User.ID)
|
||||||
lp, err := app.db.GetUserLastPostTime(p.User.ID)
|
lp, err := app.db.GetUserLastPostTime(p.User.ID)
|
||||||
|
@ -254,6 +265,38 @@ func handleAdminToggleUserStatus(app *App, u *User, w http.ResponseWriter, r *ht
|
||||||
return impart.HTTPError{http.StatusFound, fmt.Sprintf("/admin/user/%s#status", username)}
|
return impart.HTTPError{http.StatusFound, fmt.Sprintf("/admin/user/%s#status", username)}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleAdminResetUserPass(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
username := vars["username"]
|
||||||
|
if username == "" {
|
||||||
|
return impart.HTTPError{http.StatusFound, "/admin/users"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate new random password since none supplied
|
||||||
|
pass := passgen.NewWordish()
|
||||||
|
hashedPass, err := auth.HashPass([]byte(pass))
|
||||||
|
if err != nil {
|
||||||
|
return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not create password hash: %v", err)}
|
||||||
|
}
|
||||||
|
|
||||||
|
userIDVal := r.FormValue("user")
|
||||||
|
log.Info("ADMIN: Changing user %s password", userIDVal)
|
||||||
|
id, err := strconv.Atoi(userIDVal)
|
||||||
|
if err != nil {
|
||||||
|
return impart.HTTPError{http.StatusBadRequest, fmt.Sprintf("Invalid user ID: %v", err)}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = app.db.ChangePassphrase(int64(id), true, "", hashedPass)
|
||||||
|
if err != nil {
|
||||||
|
return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not update passphrase: %v", err)}
|
||||||
|
}
|
||||||
|
log.Info("ADMIN: Successfully changed.")
|
||||||
|
|
||||||
|
addSessionFlash(app, w, r, fmt.Sprintf("SUCCESS: %s", pass), nil)
|
||||||
|
|
||||||
|
return impart.HTTPError{http.StatusFound, fmt.Sprintf("/admin/user/%s", username)}
|
||||||
|
}
|
||||||
|
|
||||||
func handleViewAdminPages(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
|
func handleViewAdminPages(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||||
p := struct {
|
p := struct {
|
||||||
*UserPage
|
*UserPage
|
||||||
|
|
2
app.go
2
app.go
|
@ -56,7 +56,7 @@ var (
|
||||||
debugging bool
|
debugging bool
|
||||||
|
|
||||||
// Software version can be set from git env using -ldflags
|
// Software version can be set from git env using -ldflags
|
||||||
softwareVer = "0.10.0"
|
softwareVer = "0.11.0"
|
||||||
|
|
||||||
// DEPRECATED VARS
|
// DEPRECATED VARS
|
||||||
isSingleUser bool
|
isSingleUser bool
|
||||||
|
|
|
@ -339,7 +339,7 @@ func (c *Collection) RenderMathJax() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func newCollection(app *App, w http.ResponseWriter, r *http.Request) error {
|
func newCollection(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||||
reqJSON := IsJSON(r.Header.Get("Content-Type"))
|
reqJSON := IsJSON(r)
|
||||||
alias := r.FormValue("alias")
|
alias := r.FormValue("alias")
|
||||||
title := r.FormValue("title")
|
title := r.FormValue("title")
|
||||||
|
|
||||||
|
@ -464,7 +464,7 @@ func fetchCollection(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||||
c.hostName = app.cfg.App.Host
|
c.hostName = app.cfg.App.Host
|
||||||
|
|
||||||
// Redirect users who aren't requesting JSON
|
// Redirect users who aren't requesting JSON
|
||||||
reqJSON := IsJSON(r.Header.Get("Content-Type"))
|
reqJSON := IsJSON(r)
|
||||||
if !reqJSON {
|
if !reqJSON {
|
||||||
return impart.HTTPError{http.StatusFound, c.CanonicalURL()}
|
return impart.HTTPError{http.StatusFound, c.CanonicalURL()}
|
||||||
}
|
}
|
||||||
|
@ -943,7 +943,7 @@ func handleCollectionPostRedirect(app *App, w http.ResponseWriter, r *http.Reque
|
||||||
}
|
}
|
||||||
|
|
||||||
func existingCollection(app *App, w http.ResponseWriter, r *http.Request) error {
|
func existingCollection(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||||
reqJSON := IsJSON(r.Header.Get("Content-Type"))
|
reqJSON := IsJSON(r)
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
collAlias := vars["alias"]
|
collAlias := vars["alias"]
|
||||||
isWeb := r.FormValue("web") == "1"
|
isWeb := r.FormValue("web") == "1"
|
||||||
|
|
|
@ -20,7 +20,7 @@ import (
|
||||||
"github.com/writeas/web-core/log"
|
"github.com/writeas/web-core/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
func exportPostsCSV(u *User, posts *[]PublicPost) []byte {
|
func exportPostsCSV(hostName string, u *User, posts *[]PublicPost) []byte {
|
||||||
var b bytes.Buffer
|
var b bytes.Buffer
|
||||||
|
|
||||||
r := [][]string{
|
r := [][]string{
|
||||||
|
@ -30,8 +30,9 @@ func exportPostsCSV(u *User, posts *[]PublicPost) []byte {
|
||||||
var blog string
|
var blog string
|
||||||
if p.Collection != nil {
|
if p.Collection != nil {
|
||||||
blog = p.Collection.Alias
|
blog = p.Collection.Alias
|
||||||
|
p.Collection.hostName = hostName
|
||||||
}
|
}
|
||||||
f := []string{p.ID, p.Slug.String, blog, p.CanonicalURL(), p.Created8601(), p.Title.String, strings.Replace(p.Content, "\n", "\\n", -1)}
|
f := []string{p.ID, p.Slug.String, blog, p.CanonicalURL(hostName), p.Created8601(), p.Title.String, strings.Replace(p.Content, "\n", "\\n", -1)}
|
||||||
r = append(r, f)
|
r = append(r, f)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -45,7 +45,7 @@ require (
|
||||||
github.com/writeas/openssl-go v1.0.0 // indirect
|
github.com/writeas/openssl-go v1.0.0 // indirect
|
||||||
github.com/writeas/saturday v1.7.1
|
github.com/writeas/saturday v1.7.1
|
||||||
github.com/writeas/slug v1.2.0
|
github.com/writeas/slug v1.2.0
|
||||||
github.com/writeas/web-core v1.0.0
|
github.com/writeas/web-core v1.2.0
|
||||||
github.com/writefreely/go-nodeinfo v1.2.0
|
github.com/writefreely/go-nodeinfo v1.2.0
|
||||||
golang.org/x/crypto v0.0.0-20190208162236-193df9c0f06f
|
golang.org/x/crypto v0.0.0-20190208162236-193df9c0f06f
|
||||||
golang.org/x/lint v0.0.0-20181217174547-8f45f776aaf1 // indirect
|
golang.org/x/lint v0.0.0-20181217174547-8f45f776aaf1 // indirect
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -135,6 +135,8 @@ github.com/writeas/slug v1.2.0 h1:EMQ+cwLiOcA6EtFwUgyw3Ge18x9uflUnOnR6bp/J+/g=
|
||||||
github.com/writeas/slug v1.2.0/go.mod h1:RE8shOqQP3YhsfsQe0L3RnuejfQ4Mk+JjY5YJQFubfQ=
|
github.com/writeas/slug v1.2.0/go.mod h1:RE8shOqQP3YhsfsQe0L3RnuejfQ4Mk+JjY5YJQFubfQ=
|
||||||
github.com/writeas/web-core v1.0.0 h1:5VKkCakQgdKZcbfVKJXtRpc5VHrkflusCl/KRCPzpQ0=
|
github.com/writeas/web-core v1.0.0 h1:5VKkCakQgdKZcbfVKJXtRpc5VHrkflusCl/KRCPzpQ0=
|
||||||
github.com/writeas/web-core v1.0.0/go.mod h1:Si3chV7VWgY8CsV+3gRolMXSO2Vx1ZFAQ/mkrpvmyEE=
|
github.com/writeas/web-core v1.0.0/go.mod h1:Si3chV7VWgY8CsV+3gRolMXSO2Vx1ZFAQ/mkrpvmyEE=
|
||||||
|
github.com/writeas/web-core v1.2.0 h1:CYqvBd+byi1cK4mCr1NZ6CjILuMOFmiFecv+OACcmG0=
|
||||||
|
github.com/writeas/web-core v1.2.0/go.mod h1:vTYajviuNBAxjctPp2NUYdgjofywVkxUGpeaERF3SfI=
|
||||||
github.com/writefreely/go-nodeinfo v1.2.0 h1:La+YbTCvmpTwFhBSlebWDDL81N88Qf/SCAvRLR7F8ss=
|
github.com/writefreely/go-nodeinfo v1.2.0 h1:La+YbTCvmpTwFhBSlebWDDL81N88Qf/SCAvRLR7F8ss=
|
||||||
github.com/writefreely/go-nodeinfo v1.2.0/go.mod h1:UTvE78KpcjYOlRHupZIiSEFcXHioTXuacCbHU+CAcPg=
|
github.com/writefreely/go-nodeinfo v1.2.0/go.mod h1:UTvE78KpcjYOlRHupZIiSEFcXHioTXuacCbHU+CAcPg=
|
||||||
golang.org/x/crypto v0.0.0-20180527072434-ab813273cd59 h1:hk3yo72LXLapY9EXVttc3Z1rLOxT9IuAPPX3GpY2+jo=
|
golang.org/x/crypto v0.0.0-20180527072434-ab813273cd59 h1:hk3yo72LXLapY9EXVttc3Z1rLOxT9IuAPPX3GpY2+jo=
|
||||||
|
|
|
@ -772,7 +772,7 @@ func (h *Handler) handleError(w http.ResponseWriter, r *http.Request, err error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if IsJSON(r.Header.Get("Content-Type")) {
|
if IsJSON(r) {
|
||||||
impart.WriteError(w, impart.HTTPError{http.StatusInternalServerError, "This is an unhelpful error message for a miscellaneous internal error."})
|
impart.WriteError(w, impart.HTTPError{http.StatusInternalServerError, "This is an unhelpful error message for a miscellaneous internal error."})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
10
posts.go
10
posts.go
|
@ -483,7 +483,7 @@ func handleViewPost(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||||
// /posts?collection={alias}
|
// /posts?collection={alias}
|
||||||
// ? /collections/{alias}/posts
|
// ? /collections/{alias}/posts
|
||||||
func newPost(app *App, w http.ResponseWriter, r *http.Request) error {
|
func newPost(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||||
reqJSON := IsJSON(r.Header.Get("Content-Type"))
|
reqJSON := IsJSON(r)
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
collAlias := vars["alias"]
|
collAlias := vars["alias"]
|
||||||
if collAlias == "" {
|
if collAlias == "" {
|
||||||
|
@ -617,7 +617,7 @@ func newPost(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func existingPost(app *App, w http.ResponseWriter, r *http.Request) error {
|
func existingPost(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||||
reqJSON := IsJSON(r.Header.Get("Content-Type"))
|
reqJSON := IsJSON(r)
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
postID := vars["post"]
|
postID := vars["post"]
|
||||||
|
|
||||||
|
@ -1118,9 +1118,9 @@ func (p *Post) processPost() PublicPost {
|
||||||
return *res
|
return *res
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PublicPost) CanonicalURL() string {
|
func (p *PublicPost) CanonicalURL(hostName string) string {
|
||||||
if p.Collection == nil || p.Collection.Alias == "" {
|
if p.Collection == nil || p.Collection.Alias == "" {
|
||||||
return p.Collection.hostName + "/" + p.ID
|
return hostName + "/" + p.ID
|
||||||
}
|
}
|
||||||
return p.Collection.CanonicalURL() + p.Slug.String
|
return p.Collection.CanonicalURL() + p.Slug.String
|
||||||
}
|
}
|
||||||
|
@ -1129,7 +1129,7 @@ func (p *PublicPost) ActivityObject(cfg *config.Config) *activitystreams.Object
|
||||||
o := activitystreams.NewArticleObject()
|
o := activitystreams.NewArticleObject()
|
||||||
o.ID = p.Collection.FederatedAPIBase() + "api/posts/" + p.ID
|
o.ID = p.Collection.FederatedAPIBase() + "api/posts/" + p.ID
|
||||||
o.Published = p.Created
|
o.Published = p.Created
|
||||||
o.URL = p.CanonicalURL()
|
o.URL = p.CanonicalURL(cfg.App.Host)
|
||||||
o.AttributedTo = p.Collection.FederatedAccount()
|
o.AttributedTo = p.Collection.FederatedAccount()
|
||||||
o.CC = []string{
|
o.CC = []string{
|
||||||
p.Collection.FederatedAccount() + "/followers",
|
p.Collection.FederatedAccount() + "/followers",
|
||||||
|
|
2
read.go
2
read.go
|
@ -295,7 +295,7 @@ func viewLocalTimelineFeed(app *App, w http.ResponseWriter, req *http.Request) e
|
||||||
}
|
}
|
||||||
|
|
||||||
title = p.PlainDisplayTitle()
|
title = p.PlainDisplayTitle()
|
||||||
permalink = p.CanonicalURL()
|
permalink = p.CanonicalURL(app.cfg.App.Host)
|
||||||
if p.Collection != nil {
|
if p.Collection != nil {
|
||||||
author = p.Collection.Title
|
author = p.Collection.Title
|
||||||
} else {
|
} else {
|
||||||
|
|
12
request.go
12
request.go
|
@ -10,9 +10,13 @@
|
||||||
|
|
||||||
package writefreely
|
package writefreely
|
||||||
|
|
||||||
import "mime"
|
import (
|
||||||
|
"mime"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
func IsJSON(h string) bool {
|
func IsJSON(r *http.Request) bool {
|
||||||
ct, _, _ := mime.ParseMediaType(h)
|
ct, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||||
return ct == "application/json"
|
accept := r.Header.Get("Accept")
|
||||||
|
return ct == "application/json" || accept == "application/json"
|
||||||
}
|
}
|
||||||
|
|
|
@ -145,6 +145,7 @@ func InitRoutes(apper Apper, r *mux.Router) *mux.Router {
|
||||||
write.HandleFunc("/admin/users", handler.Admin(handleViewAdminUsers)).Methods("GET")
|
write.HandleFunc("/admin/users", handler.Admin(handleViewAdminUsers)).Methods("GET")
|
||||||
write.HandleFunc("/admin/user/{username}", handler.Admin(handleViewAdminUser)).Methods("GET")
|
write.HandleFunc("/admin/user/{username}", handler.Admin(handleViewAdminUser)).Methods("GET")
|
||||||
write.HandleFunc("/admin/user/{username}/status", handler.Admin(handleAdminToggleUserStatus)).Methods("POST")
|
write.HandleFunc("/admin/user/{username}/status", handler.Admin(handleAdminToggleUserStatus)).Methods("POST")
|
||||||
|
write.HandleFunc("/admin/user/{username}/passphrase", handler.Admin(handleAdminResetUserPass)).Methods("POST")
|
||||||
write.HandleFunc("/admin/pages", handler.Admin(handleViewAdminPages)).Methods("GET")
|
write.HandleFunc("/admin/pages", handler.Admin(handleViewAdminPages)).Methods("GET")
|
||||||
write.HandleFunc("/admin/page/{slug}", handler.Admin(handleViewAdminPage)).Methods("GET")
|
write.HandleFunc("/admin/page/{slug}", handler.Admin(handleViewAdminPage)).Methods("GET")
|
||||||
write.HandleFunc("/admin/update/config", handler.AdminApper(handleAdminUpdateConfig)).Methods("POST")
|
write.HandleFunc("/admin/update/config", handler.AdminApper(handleAdminUpdateConfig)).Methods("POST")
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
<link rel="stylesheet" type="text/css" href="/css/write.css" />
|
<link rel="stylesheet" type="text/css" href="/css/write.css" />
|
||||||
<link rel="shortcut icon" href="/favicon.ico" />
|
<link rel="shortcut icon" href="/favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<link rel="canonical" href="{{.CanonicalURL}}" />
|
<link rel="canonical" href="{{.CanonicalURL .Host}}" />
|
||||||
<meta name="generator" content="WriteFreely">
|
<meta name="generator" content="WriteFreely">
|
||||||
<meta name="title" content="{{.PlainDisplayTitle}} {{localhtml "title dash" .Language.String}} {{if .Collection.Title}}{{.Collection.Title}}{{else}}{{.Collection.Alias}}{{end}}">
|
<meta name="title" content="{{.PlainDisplayTitle}} {{localhtml "title dash" .Language.String}} {{if .Collection.Title}}{{.Collection.Title}}{{else}}{{.Collection.Alias}}{{end}}">
|
||||||
<meta name="description" content="{{.Summary}}">
|
<meta name="description" content="{{.Summary}}">
|
||||||
|
@ -25,7 +25,7 @@
|
||||||
<meta property="og:description" content="{{.Summary}}" />
|
<meta property="og:description" content="{{.Summary}}" />
|
||||||
<meta property="og:site_name" content="{{.Collection.DisplayTitle}}" />
|
<meta property="og:site_name" content="{{.Collection.DisplayTitle}}" />
|
||||||
<meta property="og:type" content="article" />
|
<meta property="og:type" content="article" />
|
||||||
<meta property="og:url" content="{{.CanonicalURL}}" />
|
<meta property="og:url" content="{{.CanonicalURL .Host}}" />
|
||||||
<meta property="og:updated_time" content="{{.Created8601}}" />
|
<meta property="og:updated_time" content="{{.Created8601}}" />
|
||||||
{{range .Images}}<meta property="og:image" content="{{.}}" />{{else}}<meta property="og:image" content="{{.Collection.AvatarURL}}">{{end}}
|
{{range .Images}}<meta property="og:image" content="{{.}}" />{{else}}<meta property="og:image" content="{{.Collection.AvatarURL}}">{{end}}
|
||||||
<meta property="article:published_time" content="{{.Created8601}}">
|
<meta property="article:published_time" content="{{.Created8601}}">
|
||||||
|
@ -80,7 +80,7 @@ article time.dt-published {
|
||||||
</p>
|
</p>
|
||||||
<nav>
|
<nav>
|
||||||
{{if .PinnedPosts}}
|
{{if .PinnedPosts}}
|
||||||
{{range .PinnedPosts}}<a class="pinned{{if eq .Slug.String $.Slug.String}} selected{{end}}" href="{{if not $.SingleUser}}/{{$.Collection.Alias}}/{{.Slug.String}}{{else}}{{.CanonicalURL}}{{end}}">{{.PlainDisplayTitle}}</a>{{end}}
|
{{range .PinnedPosts}}<a class="pinned{{if eq .Slug.String $.Slug.String}} selected{{end}}" href="{{if not $.SingleUser}}/{{$.Collection.Alias}}/{{.Slug.String}}{{else}}{{.CanonicalURL .Host}}{{end}}">{{.PlainDisplayTitle}}</a>{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
</nav>
|
</nav>
|
||||||
<hr>
|
<hr>
|
||||||
|
|
|
@ -71,7 +71,7 @@ body#collection header nav.tabs a:first-child {
|
||||||
<!--p class="meta-note"><span>Private collection</span>. Only you can see this page.</p-->
|
<!--p class="meta-note"><span>Private collection</span>. Only you can see this page.</p-->
|
||||||
{{/*end*/}}
|
{{/*end*/}}
|
||||||
{{if .PinnedPosts}}<nav class="pinned-posts">
|
{{if .PinnedPosts}}<nav class="pinned-posts">
|
||||||
{{range .PinnedPosts}}<a class="pinned" href="{{if not $.SingleUser}}/{{$.Alias}}/{{.Slug.String}}{{else}}{{.CanonicalURL}}{{end}}">{{.PlainDisplayTitle}}</a>{{end}}</nav>
|
{{range .PinnedPosts}}<a class="pinned" href="{{if not $.SingleUser}}/{{$.Alias}}/{{.Slug.String}}{{else}}{{.CanonicalURL .Host}}{{end}}">{{.PlainDisplayTitle}}</a>{{end}}</nav>
|
||||||
{{end}}
|
{{end}}
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
<link rel="shortcut icon" href="/favicon.ico" />
|
<link rel="shortcut icon" href="/favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
{{ if .IsFound }}
|
{{ if .IsFound }}
|
||||||
<link rel="canonical" href="{{.CanonicalURL}}" />
|
<link rel="canonical" href="{{.CanonicalURL .Host}}" />
|
||||||
<meta name="generator" content="WriteFreely">
|
<meta name="generator" content="WriteFreely">
|
||||||
<meta name="title" content="{{.PlainDisplayTitle}} {{localhtml "title dash" .Language.String}} {{if .Collection.Title}}{{.Collection.Title}}{{else}}{{.Collection.Alias}}{{end}}">
|
<meta name="title" content="{{.PlainDisplayTitle}} {{localhtml "title dash" .Language.String}} {{if .Collection.Title}}{{.Collection.Title}}{{else}}{{.Collection.Alias}}{{end}}">
|
||||||
<meta name="description" content="{{.Summary}}">
|
<meta name="description" content="{{.Summary}}">
|
||||||
|
@ -26,7 +26,7 @@
|
||||||
<meta property="og:description" content="{{.Summary}}" />
|
<meta property="og:description" content="{{.Summary}}" />
|
||||||
<meta property="og:site_name" content="{{.Collection.DisplayTitle}}" />
|
<meta property="og:site_name" content="{{.Collection.DisplayTitle}}" />
|
||||||
<meta property="og:type" content="article" />
|
<meta property="og:type" content="article" />
|
||||||
<meta property="og:url" content="{{.CanonicalURL}}" />
|
<meta property="og:url" content="{{.CanonicalURL .Host}}" />
|
||||||
<meta property="og:updated_time" content="{{.Created8601}}" />
|
<meta property="og:updated_time" content="{{.Created8601}}" />
|
||||||
{{range .Images}}<meta property="og:image" content="{{.}}" />{{else}}<meta property="og:image" content="{{.Collection.AvatarURL}}">{{end}}
|
{{range .Images}}<meta property="og:image" content="{{.}}" />{{else}}<meta property="og:image" content="{{.Collection.AvatarURL}}">{{end}}
|
||||||
<meta property="article:published_time" content="{{.Created8601}}">
|
<meta property="article:published_time" content="{{.Created8601}}">
|
||||||
|
@ -50,7 +50,7 @@
|
||||||
<h1 dir="{{.Direction}}" id="blog-title"><a rel="author" href="{{if .IsTopLevel}}/{{else}}/{{.Collection.Alias}}/{{end}}" class="h-card p-author">{{.Collection.DisplayTitle}}</a></h1>
|
<h1 dir="{{.Direction}}" id="blog-title"><a rel="author" href="{{if .IsTopLevel}}/{{else}}/{{.Collection.Alias}}/{{end}}" class="h-card p-author">{{.Collection.DisplayTitle}}</a></h1>
|
||||||
<nav>
|
<nav>
|
||||||
{{if .PinnedPosts}}
|
{{if .PinnedPosts}}
|
||||||
{{range .PinnedPosts}}<a class="pinned{{if eq .Slug.String $.Slug.String}} selected{{end}}" href="{{if not $.SingleUser}}/{{$.Collection.Alias}}/{{.Slug.String}}{{else}}{{.CanonicalURL}}{{end}}">{{.PlainDisplayTitle}}</a>{{end}}
|
{{range .PinnedPosts}}<a class="pinned{{if eq .Slug.String $.Slug.String}} selected{{end}}" href="{{if not $.SingleUser}}/{{$.Collection.Alias}}/{{.Slug.String}}{{else}}{{.CanonicalURL .Host}}{{end}}">{{.PlainDisplayTitle}}</a>{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
{{ if and .IsOwner .IsFound }}<span class="views" dir="ltr"><strong>{{largeNumFmt .Views}}</strong> {{pluralize "view" "views" .Views}}</span>
|
{{ if and .IsOwner .IsFound }}<span class="views" dir="ltr"><strong>{{largeNumFmt .Views}}</strong> {{pluralize "view" "views" .Views}}</span>
|
||||||
<a class="xtra-feature" href="/{{if not .SingleUser}}{{.Collection.Alias}}/{{end}}{{.Slug.String}}/edit" dir="{{.Direction}}">Edit</a>
|
<a class="xtra-feature" href="/{{if not .SingleUser}}{{.Collection.Alias}}/{{end}}{{.Slug.String}}/edit" dir="{{.Direction}}">Edit</a>
|
||||||
|
|
|
@ -48,7 +48,7 @@
|
||||||
<h1 dir="{{.Direction}}" id="blog-title"><a href="{{if .IsTopLevel}}/{{else}}/{{.Collection.Alias}}/{{end}}" class="h-card p-author">{{.Collection.DisplayTitle}}</a></h1>
|
<h1 dir="{{.Direction}}" id="blog-title"><a href="{{if .IsTopLevel}}/{{else}}/{{.Collection.Alias}}/{{end}}" class="h-card p-author">{{.Collection.DisplayTitle}}</a></h1>
|
||||||
<nav>
|
<nav>
|
||||||
{{if .PinnedPosts}}
|
{{if .PinnedPosts}}
|
||||||
{{range .PinnedPosts}}<a class="pinned" href="{{if not $.SingleUser}}/{{$.Collection.Alias}}/{{.Slug.String}}{{else}}{{.CanonicalURL}}{{end}}">{{.DisplayTitle}}</a>{{end}}
|
{{range .PinnedPosts}}<a class="pinned" href="{{if not $.SingleUser}}/{{$.Collection.Alias}}/{{.Slug.String}}{{else}}{{.CanonicalURL .Host}}{{end}}">{{.DisplayTitle}}</a>{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
|
@ -71,7 +71,7 @@
|
||||||
<!--p class="meta-note"><span>Private collection</span>. Only you can see this page.</p-->
|
<!--p class="meta-note"><span>Private collection</span>. Only you can see this page.</p-->
|
||||||
{{/*end*/}}
|
{{/*end*/}}
|
||||||
{{if .PinnedPosts}}<nav>
|
{{if .PinnedPosts}}<nav>
|
||||||
{{range .PinnedPosts}}<a class="pinned" href="{{if not $.SingleUser}}/{{$.Alias}}/{{.Slug.String}}{{else}}{{.CanonicalURL}}{{end}}">{{.PlainDisplayTitle}}</a>{{end}}</nav>
|
{{range .PinnedPosts}}<a class="pinned" href="{{if not $.SingleUser}}/{{$.Alias}}/{{.Slug.String}}{{else}}{{.CanonicalURL .Host}}{{end}}">{{.PlainDisplayTitle}}</a>{{end}}</nav>
|
||||||
{{end}}
|
{{end}}
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|
|
@ -25,10 +25,10 @@
|
||||||
{{else}}<li><a id="publish-to"><span id="target-name">Draft</span> <img class="ic-18dp" src="/img/ic_down_arrow_dark@2x.png" /></a>
|
{{else}}<li><a id="publish-to"><span id="target-name">Draft</span> <img class="ic-18dp" src="/img/ic_down_arrow_dark@2x.png" /></a>
|
||||||
<ul>
|
<ul>
|
||||||
<li class="menu-heading">Publish to...</li>
|
<li class="menu-heading">Publish to...</li>
|
||||||
<li class="target selected" id="anonymous"><a href="#anonymous"><i class="material-icons md-18">description</i> <em>Draft</em></a></li>
|
{{if .Blogs}}{{range $idx, $el := .Blogs}}
|
||||||
{{if .Blogs}}{{range .Blogs}}
|
<li class="target{{if eq $idx 0}} selected{{end}}" id="blog-{{$el.Alias}}"><a href="#{{$el.Alias}}"><i class="material-icons md-18">public</i> {{if $el.Title}}{{$el.Title}}{{else}}{{$el.Alias}}{{end}}</a></li>
|
||||||
<li class="target" id="blog-{{.Alias}}"><a href="#{{.Alias}}"><i class="material-icons md-18">public</i> {{if .Title}}{{.Title}}{{else}}{{.Alias}}{{end}}</a></li>
|
|
||||||
{{end}}{{end}}
|
{{end}}{{end}}
|
||||||
|
<li class="target" id="blog-anonymous"><a href="#anonymous"><i class="material-icons md-18">description</i> <em>Draft</em></a></li>
|
||||||
<li id="user-separator" class="separator"><hr /></li>
|
<li id="user-separator" class="separator"><hr /></li>
|
||||||
{{ if .SingleUser }}
|
{{ if .SingleUser }}
|
||||||
<li><a href="/"><i class="material-icons md-18">launch</i> View Blog</a></li>
|
<li><a href="/"><i class="material-icons md-18">launch</i> View Blog</a></li>
|
||||||
|
@ -282,7 +282,7 @@
|
||||||
document.getElementById('target-name').innerText = newText.join(' ');
|
document.getElementById('target-name').innerText = newText.join(' ');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
var postTarget = H.get('postTarget', 'anonymous');
|
var postTarget = H.get('postTarget', '{{if .Blogs}}{{$blog := index .Blogs 0}}{{$blog.Alias}}{{else}}anonymous{{end}}');
|
||||||
if (location.hash != '') {
|
if (location.hash != '') {
|
||||||
postTarget = location.hash.substring(1);
|
postTarget = location.hash.substring(1);
|
||||||
// TODO: pushState to /pad (or whatever the URL is) so we live on a clean URL
|
// TODO: pushState to /pad (or whatever the URL is) so we live on a clean URL
|
||||||
|
|
|
@ -87,17 +87,17 @@
|
||||||
{{ if gt (len .Posts) 0 }}
|
{{ if gt (len .Posts) 0 }}
|
||||||
<section itemscope itemtype="http://schema.org/Blog">
|
<section itemscope itemtype="http://schema.org/Blog">
|
||||||
{{range .Posts}}<article class="{{.Font}} h-entry" itemscope itemtype="http://schema.org/BlogPosting">
|
{{range .Posts}}<article class="{{.Font}} h-entry" itemscope itemtype="http://schema.org/BlogPosting">
|
||||||
{{if .Title.String}}<h2 class="post-title" itemprop="name" class="p-name"><a href="{{if .Slug.String}}{{.Collection.CanonicalURL}}{{.Slug.String}}{{else}}{{.CanonicalURL}}.md{{end}}" itemprop="url" class="u-url">{{.PlainDisplayTitle}}</a></h2>
|
{{if .Title.String}}<h2 class="post-title" itemprop="name" class="p-name"><a href="{{if .Slug.String}}{{.Collection.CanonicalURL}}{{.Slug.String}}{{else}}{{.CanonicalURL .Host}}.md{{end}}" itemprop="url" class="u-url">{{.PlainDisplayTitle}}</a></h2>
|
||||||
<time class="dt-published" datetime="{{.Created}}" pubdate itemprop="datePublished" content="{{.Created}}">{{if not .Title.String}}<a href="{{.Collection.CanonicalURL}}{{.Slug.String}}" itemprop="url">{{end}}{{.DisplayDate}}{{if not .Title.String}}</a>{{end}}</time>
|
<time class="dt-published" datetime="{{.Created}}" pubdate itemprop="datePublished" content="{{.Created}}">{{if not .Title.String}}<a href="{{.Collection.CanonicalURL}}{{.Slug.String}}" itemprop="url">{{end}}{{.DisplayDate}}{{if not .Title.String}}</a>{{end}}</time>
|
||||||
{{else}}
|
{{else}}
|
||||||
<h2 class="post-title" itemprop="name"><time class="dt-published" datetime="{{.Created}}" pubdate itemprop="datePublished" content="{{.Created}}"><a href="{{if .Collection}}{{.Collection.CanonicalURL}}{{.Slug.String}}{{else}}{{.CanonicalURL}}.md{{end}}" itemprop="url" class="u-url">{{.DisplayDate}}</a></time></h2>
|
<h2 class="post-title" itemprop="name"><time class="dt-published" datetime="{{.Created}}" pubdate itemprop="datePublished" content="{{.Created}}"><a href="{{if .Collection}}{{.Collection.CanonicalURL}}{{.Slug.String}}{{else}}{{.CanonicalURL .Host}}.md{{end}}" itemprop="url" class="u-url">{{.DisplayDate}}</a></time></h2>
|
||||||
{{end}}
|
{{end}}
|
||||||
<p class="source">{{if .Collection}}from <a href="{{.Collection.CanonicalURL}}">{{.Collection.DisplayTitle}}</a>{{else}}<em>Anonymous</em>{{end}}</p>
|
<p class="source">{{if .Collection}}from <a href="{{.Collection.CanonicalURL}}">{{.Collection.DisplayTitle}}</a>{{else}}<em>Anonymous</em>{{end}}</p>
|
||||||
{{if .Excerpt}}<div class="p-summary" {{if .Language}}lang="{{.Language.String}}"{{end}} dir="{{.Direction}}">{{.Excerpt}}</div>
|
{{if .Excerpt}}<div class="p-summary" {{if .Language}}lang="{{.Language.String}}"{{end}} dir="{{.Direction}}">{{.Excerpt}}</div>
|
||||||
|
|
||||||
<a class="read-more" href="{{if .Collection}}{{.Collection.CanonicalURL}}{{.Slug.String}}{{else}}{{.CanonicalURL}}.md{{end}}">{{localstr "Read more..." .Language.String}}</a>{{else}}<div class="e-content preview" {{if .Language}}lang="{{.Language.String}}"{{end}} dir="{{.Direction}}">{{ if not .HTMLContent }}<p id="post-body" class="e-content preview">{{.Content}}</p>{{ else }}{{.HTMLContent}}{{ end }}<div class="over"> </div></div>
|
<a class="read-more" href="{{if .Collection}}{{.Collection.CanonicalURL}}{{.Slug.String}}{{else}}{{.CanonicalURL .Host}}.md{{end}}">{{localstr "Read more..." .Language.String}}</a>{{else}}<div class="e-content preview" {{if .Language}}lang="{{.Language.String}}"{{end}} dir="{{.Direction}}">{{ if not .HTMLContent }}<p id="post-body" class="e-content preview">{{.Content}}</p>{{ else }}{{.HTMLContent}}{{ end }}<div class="over"> </div></div>
|
||||||
|
|
||||||
<a class="read-more maybe" href="{{if .Collection}}{{.Collection.CanonicalURL}}{{.Slug.String}}{{else}}{{.CanonicalURL}}.md{{end}}">{{localstr "Read more..." .Language.String}}</a>{{end}}</article>
|
<a class="read-more maybe" href="{{if .Collection}}{{.Collection.CanonicalURL}}{{.Slug.String}}{{else}}{{.CanonicalURL .Host}}.md{{end}}">{{localstr "Read more..." .Language.String}}</a>{{end}}</article>
|
||||||
{{end}}
|
{{end}}
|
||||||
</section>
|
</section>
|
||||||
{{ else }}
|
{{ else }}
|
||||||
|
|
|
@ -25,12 +25,25 @@ td.active-suspend > input[type="submit"] {
|
||||||
margin: auto;
|
margin: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
input.copy-text {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.2em;
|
||||||
|
color: #555;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<div class="snug content-container">
|
<div class="snug content-container">
|
||||||
{{template "admin-header" .}}
|
{{template "admin-header" .}}
|
||||||
|
|
||||||
<h2 id="posts-header">{{.User.Username}}</h2>
|
<h2 id="posts-header">{{.User.Username}}</h2>
|
||||||
|
{{if .NewPassword}}<div class="alert success">
|
||||||
|
<p>This user's password has been reset to:</p>
|
||||||
|
<p><input type="text" class="copy-text" value="{{.NewPassword}}" onfocus="if (this.select) this.select(); else this.setSelectionRange(0, this.value.length);" readonly /></p>
|
||||||
|
<p>They can use this new password to log in to their account. <strong>This will only be shown once</strong>, so be sure to copy it and send it to them now.</p>
|
||||||
|
{{if .ClearEmail}}<p>Their email address is: <a href="mailto:{{.ClearEmail}}">{{.ClearEmail}}</a></p>{{end}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
<table class="classy export">
|
<table class="classy export">
|
||||||
<tr>
|
<tr>
|
||||||
<th>No.</th>
|
<th>No.</th>
|
||||||
|
@ -71,6 +84,19 @@ td.active-suspend > input[type="submit"] {
|
||||||
</td>
|
</td>
|
||||||
</form>
|
</form>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Password</th>
|
||||||
|
<td>
|
||||||
|
{{if ne .Username .User.Username}}
|
||||||
|
<form id="reset-form" action="/admin/user/{{.User.Username}}/passphrase" method="post" autocomplete="false">
|
||||||
|
<input type="hidden" name="user" value="{{.User.ID}}"/>
|
||||||
|
<button type="submit">Reset</button>
|
||||||
|
</form>
|
||||||
|
{{else}}
|
||||||
|
<a href="/me/settings" title="Go to reset password page">Change your password</a>
|
||||||
|
{{end}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<h2>Blogs</h2>
|
<h2>Blogs</h2>
|
||||||
|
@ -120,7 +146,15 @@ td.active-suspend > input[type="submit"] {
|
||||||
function confirmSilence() {
|
function confirmSilence() {
|
||||||
return confirm("Silence this user? They'll still be able to log in and access their posts, but no one else will be able to see them anymore. You can reverse this decision at any time.");
|
return confirm("Silence this user? They'll still be able to log in and access their posts, but no one else will be able to see them anymore. You can reverse this decision at any time.");
|
||||||
}
|
}
|
||||||
</script>
|
|
||||||
|
|
||||||
|
form = document.getElementById("reset-form");
|
||||||
|
form.addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
agreed = confirm("Reset this user's password? This will generate a new temporary password that you'll need to share with them, and invalidate their old one.");
|
||||||
|
if (agreed === true) {
|
||||||
|
form.submit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
{{template "footer" .}}
|
{{template "footer" .}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
@ -13,13 +13,14 @@ package writefreely
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
"github.com/writeas/impart"
|
"github.com/writeas/impart"
|
||||||
"github.com/writeas/web-core/log"
|
"github.com/writeas/web-core/log"
|
||||||
"net/http"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func handleWebSignup(app *App, w http.ResponseWriter, r *http.Request) error {
|
func handleWebSignup(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||||
reqJSON := IsJSON(r.Header.Get("Content-Type"))
|
reqJSON := IsJSON(r)
|
||||||
|
|
||||||
// Get params
|
// Get params
|
||||||
var ur userRegistration
|
var ur userRegistration
|
||||||
|
@ -71,7 +72,7 @@ func handleWebSignup(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||||
// { "username": "asdf" }
|
// { "username": "asdf" }
|
||||||
// result: { code: 204 }
|
// result: { code: 204 }
|
||||||
func handleUsernameCheck(app *App, w http.ResponseWriter, r *http.Request) error {
|
func handleUsernameCheck(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||||
reqJSON := IsJSON(r.Header.Get("Content-Type"))
|
reqJSON := IsJSON(r)
|
||||||
|
|
||||||
// Get params
|
// Get params
|
||||||
var d struct {
|
var d struct {
|
||||||
|
|
Loading…
Reference in a new issue