Merge branch 'develop' into T661-disable-accounts

pull/174/head
Matt Baer 5 years ago
commit 53586d9cb8
  1. 10
      README.md
  2. 18
      account.go
  3. 4
      activitypub.go
  4. 45
      admin.go
  5. 2
      app.go
  6. 6
      collections.go
  7. 5
      export.go
  8. 2
      go.mod
  9. 2
      go.sum
  10. 2
      handle.go
  11. 10
      posts.go
  12. 2
      read.go
  13. 12
      request.go
  14. 1
      routes.go
  15. 6
      templates/chorus-collection-post.tmpl
  16. 2
      templates/chorus-collection.tmpl
  17. 6
      templates/collection-post.tmpl
  18. 2
      templates/collection-tags.tmpl
  19. 2
      templates/collection.tmpl
  20. 8
      templates/pad.tmpl
  21. 8
      templates/read.tmpl
  22. 38
      templates/user/admin/view-user.tmpl
  23. 7
      unregisteredusers.go

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

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

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

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

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

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

@ -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",

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

@ -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">&nbsp;</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">&nbsp;</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…
Cancel
Save