mirror of
https://github.com/writefreely/writefreely
synced 2025-02-17 16:28:23 +00:00
Merge branch 'develop' into activitypub-mentions
This commit is contained in:
commit
d8df15855c
47 changed files with 618 additions and 122 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
|
||||||
|
|
||||||
|
|
61
account.go
61
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
|
||||||
}
|
}
|
||||||
|
@ -750,14 +750,20 @@ func viewArticles(app *App, u *User, w http.ResponseWriter, r *http.Request) err
|
||||||
log.Error("unable to fetch collections: %v", err)
|
log.Error("unable to fetch collections: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspended, err := app.db.IsUserSuspended(u.ID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("view articles: %v", err)
|
||||||
|
}
|
||||||
d := struct {
|
d := struct {
|
||||||
*UserPage
|
*UserPage
|
||||||
AnonymousPosts *[]PublicPost
|
AnonymousPosts *[]PublicPost
|
||||||
Collections *[]Collection
|
Collections *[]Collection
|
||||||
|
Suspended bool
|
||||||
}{
|
}{
|
||||||
UserPage: NewUserPage(app, r, u, u.Username+"'s Posts", f),
|
UserPage: NewUserPage(app, r, u, u.Username+"'s Posts", f),
|
||||||
AnonymousPosts: p,
|
AnonymousPosts: p,
|
||||||
Collections: c,
|
Collections: c,
|
||||||
|
Suspended: suspended,
|
||||||
}
|
}
|
||||||
d.UserPage.SetMessaging(u)
|
d.UserPage.SetMessaging(u)
|
||||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||||
|
@ -779,6 +785,11 @@ func viewCollections(app *App, u *User, w http.ResponseWriter, r *http.Request)
|
||||||
uc, _ := app.db.GetUserCollectionCount(u.ID)
|
uc, _ := app.db.GetUserCollectionCount(u.ID)
|
||||||
// TODO: handle any errors
|
// TODO: handle any errors
|
||||||
|
|
||||||
|
suspended, err := app.db.IsUserSuspended(u.ID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("view collections %v", err)
|
||||||
|
return fmt.Errorf("view collections: %v", err)
|
||||||
|
}
|
||||||
d := struct {
|
d := struct {
|
||||||
*UserPage
|
*UserPage
|
||||||
Collections *[]Collection
|
Collections *[]Collection
|
||||||
|
@ -786,11 +797,13 @@ func viewCollections(app *App, u *User, w http.ResponseWriter, r *http.Request)
|
||||||
UsedCollections, TotalCollections int
|
UsedCollections, TotalCollections int
|
||||||
|
|
||||||
NewBlogsDisabled bool
|
NewBlogsDisabled bool
|
||||||
|
Suspended bool
|
||||||
}{
|
}{
|
||||||
UserPage: NewUserPage(app, r, u, u.Username+"'s Blogs", f),
|
UserPage: NewUserPage(app, r, u, u.Username+"'s Blogs", f),
|
||||||
Collections: c,
|
Collections: c,
|
||||||
UsedCollections: int(uc),
|
UsedCollections: int(uc),
|
||||||
NewBlogsDisabled: !app.cfg.App.CanCreateBlogs(uc),
|
NewBlogsDisabled: !app.cfg.App.CanCreateBlogs(uc),
|
||||||
|
Suspended: suspended,
|
||||||
}
|
}
|
||||||
d.UserPage.SetMessaging(u)
|
d.UserPage.SetMessaging(u)
|
||||||
showUserPage(w, "collections", d)
|
showUserPage(w, "collections", d)
|
||||||
|
@ -808,13 +821,20 @@ func viewEditCollection(app *App, u *User, w http.ResponseWriter, r *http.Reques
|
||||||
return ErrCollectionNotFound
|
return ErrCollectionNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspended, err := app.db.IsUserSuspended(u.ID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("view edit collection %v", err)
|
||||||
|
return fmt.Errorf("view edit collection: %v", err)
|
||||||
|
}
|
||||||
flashes, _ := getSessionFlashes(app, w, r, nil)
|
flashes, _ := getSessionFlashes(app, w, r, nil)
|
||||||
obj := struct {
|
obj := struct {
|
||||||
*UserPage
|
*UserPage
|
||||||
*Collection
|
*Collection
|
||||||
|
Suspended bool
|
||||||
}{
|
}{
|
||||||
UserPage: NewUserPage(app, r, u, "Edit "+c.DisplayTitle(), flashes),
|
UserPage: NewUserPage(app, r, u, "Edit "+c.DisplayTitle(), flashes),
|
||||||
Collection: c,
|
Collection: c,
|
||||||
|
Suspended: suspended,
|
||||||
}
|
}
|
||||||
|
|
||||||
showUserPage(w, "collection", obj)
|
showUserPage(w, "collection", obj)
|
||||||
|
@ -822,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
|
||||||
|
@ -976,17 +996,24 @@ func viewStats(app *App, u *User, w http.ResponseWriter, r *http.Request) error
|
||||||
titleStats = c.DisplayTitle() + " "
|
titleStats = c.DisplayTitle() + " "
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspended, err := app.db.IsUserSuspended(u.ID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("view stats: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
obj := struct {
|
obj := struct {
|
||||||
*UserPage
|
*UserPage
|
||||||
VisitsBlog string
|
VisitsBlog string
|
||||||
Collection *Collection
|
Collection *Collection
|
||||||
TopPosts *[]PublicPost
|
TopPosts *[]PublicPost
|
||||||
APFollowers int
|
APFollowers int
|
||||||
|
Suspended bool
|
||||||
}{
|
}{
|
||||||
UserPage: NewUserPage(app, r, u, titleStats+"Stats", flashes),
|
UserPage: NewUserPage(app, r, u, titleStats+"Stats", flashes),
|
||||||
VisitsBlog: alias,
|
VisitsBlog: alias,
|
||||||
Collection: c,
|
Collection: c,
|
||||||
TopPosts: topPosts,
|
TopPosts: topPosts,
|
||||||
|
Suspended: suspended,
|
||||||
}
|
}
|
||||||
if app.cfg.App.Federation {
|
if app.cfg.App.Federation {
|
||||||
folls, err := app.db.GetAPFollowers(c)
|
folls, err := app.db.GetAPFollowers(c)
|
||||||
|
@ -1017,14 +1044,16 @@ func viewSettings(app *App, u *User, w http.ResponseWriter, r *http.Request) err
|
||||||
|
|
||||||
obj := struct {
|
obj := struct {
|
||||||
*UserPage
|
*UserPage
|
||||||
Email string
|
Email string
|
||||||
HasPass bool
|
HasPass bool
|
||||||
IsLogOut bool
|
IsLogOut bool
|
||||||
|
Suspended bool
|
||||||
}{
|
}{
|
||||||
UserPage: NewUserPage(app, r, u, "Account Settings", flashes),
|
UserPage: NewUserPage(app, r, u, "Account Settings", flashes),
|
||||||
Email: fullUser.EmailClear(app.keys),
|
Email: fullUser.EmailClear(app.keys),
|
||||||
HasPass: passIsSet,
|
HasPass: passIsSet,
|
||||||
IsLogOut: r.FormValue("logout") == "1",
|
IsLogOut: r.FormValue("logout") == "1",
|
||||||
|
Suspended: fullUser.IsSilenced(),
|
||||||
}
|
}
|
||||||
|
|
||||||
showUserPage(w, "settings", obj)
|
showUserPage(w, "settings", obj)
|
||||||
|
|
|
@ -81,6 +81,14 @@ func handleFetchCollectionActivities(app *App, w http.ResponseWriter, r *http.Re
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
suspended, err := app.db.IsUserSuspended(c.OwnerID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("fetch collection activities: %v", err)
|
||||||
|
return ErrInternalGeneral
|
||||||
|
}
|
||||||
|
if suspended {
|
||||||
|
return ErrCollectionNotFound
|
||||||
|
}
|
||||||
c.hostName = app.cfg.App.Host
|
c.hostName = app.cfg.App.Host
|
||||||
|
|
||||||
p := c.PersonObject()
|
p := c.PersonObject()
|
||||||
|
@ -106,6 +114,14 @@ func handleFetchCollectionOutbox(app *App, w http.ResponseWriter, r *http.Reques
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
suspended, err := app.db.IsUserSuspended(c.OwnerID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("fetch collection outbox: %v", err)
|
||||||
|
return ErrInternalGeneral
|
||||||
|
}
|
||||||
|
if suspended {
|
||||||
|
return ErrCollectionNotFound
|
||||||
|
}
|
||||||
c.hostName = app.cfg.App.Host
|
c.hostName = app.cfg.App.Host
|
||||||
|
|
||||||
if app.cfg.App.SingleUser {
|
if app.cfg.App.SingleUser {
|
||||||
|
@ -159,6 +175,14 @@ func handleFetchCollectionFollowers(app *App, w http.ResponseWriter, r *http.Req
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
suspended, err := app.db.IsUserSuspended(c.OwnerID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("fetch collection followers: %v", err)
|
||||||
|
return ErrInternalGeneral
|
||||||
|
}
|
||||||
|
if suspended {
|
||||||
|
return ErrCollectionNotFound
|
||||||
|
}
|
||||||
c.hostName = app.cfg.App.Host
|
c.hostName = app.cfg.App.Host
|
||||||
|
|
||||||
accountRoot := c.FederatedAccount()
|
accountRoot := c.FederatedAccount()
|
||||||
|
@ -205,6 +229,14 @@ func handleFetchCollectionFollowing(app *App, w http.ResponseWriter, r *http.Req
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
suspended, err := app.db.IsUserSuspended(c.OwnerID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("fetch collection following: %v", err)
|
||||||
|
return ErrInternalGeneral
|
||||||
|
}
|
||||||
|
if suspended {
|
||||||
|
return ErrCollectionNotFound
|
||||||
|
}
|
||||||
c.hostName = app.cfg.App.Host
|
c.hostName = app.cfg.App.Host
|
||||||
|
|
||||||
accountRoot := c.FederatedAccount()
|
accountRoot := c.FederatedAccount()
|
||||||
|
@ -239,6 +271,14 @@ func handleFetchCollectionInbox(app *App, w http.ResponseWriter, r *http.Request
|
||||||
// TODO: return Reject?
|
// TODO: return Reject?
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
suspended, err := app.db.IsUserSuspended(c.OwnerID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("fetch collection inbox: %v", err)
|
||||||
|
return ErrInternalGeneral
|
||||||
|
}
|
||||||
|
if suspended {
|
||||||
|
return ErrCollectionNotFound
|
||||||
|
}
|
||||||
c.hostName = app.cfg.App.Host
|
c.hostName = app.cfg.App.Host
|
||||||
|
|
||||||
if debugging {
|
if debugging {
|
||||||
|
@ -376,11 +416,11 @@ 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
|
||||||
t.Rollback()
|
// res.LastInsertId below
|
||||||
log.Error("Couldn't add new remoteuser in DB: %v\n", err)
|
t.Rollback()
|
||||||
return
|
log.Error("Couldn't add new remoteuser in DB: %v\n", err)
|
||||||
}
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
followerID, err = res.LastInsertId()
|
followerID, err = res.LastInsertId()
|
||||||
|
|
77
admin.go
77
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"
|
||||||
)
|
)
|
||||||
|
@ -170,11 +172,12 @@ func handleViewAdminUser(app *App, u *User, w http.ResponseWriter, r *http.Reque
|
||||||
Config config.AppCfg
|
Config config.AppCfg
|
||||||
Message string
|
Message string
|
||||||
|
|
||||||
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)
|
||||||
|
@ -230,6 +241,62 @@ func handleViewAdminUser(app *App, u *User, w http.ResponseWriter, r *http.Reque
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleAdminToggleUserStatus(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"}
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := app.db.GetUserForAuth(username)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("failed to get user: %v", err)
|
||||||
|
return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get user from username: %v", err)}
|
||||||
|
}
|
||||||
|
if user.IsSilenced() {
|
||||||
|
err = app.db.SetUserStatus(user.ID, UserActive)
|
||||||
|
} else {
|
||||||
|
err = app.db.SetUserStatus(user.ID, UserSilenced)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
log.Error("toggle user suspended: %v", err)
|
||||||
|
return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not toggle user status: %v")}
|
||||||
|
}
|
||||||
|
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.1"
|
||||||
|
|
||||||
// DEPRECATED VARS
|
// DEPRECATED VARS
|
||||||
isSingleUser bool
|
isSingleUser bool
|
||||||
|
|
|
@ -71,6 +71,7 @@ type (
|
||||||
CurrentPage int
|
CurrentPage int
|
||||||
TotalPages int
|
TotalPages int
|
||||||
Format *CollectionFormat
|
Format *CollectionFormat
|
||||||
|
Suspended bool
|
||||||
}
|
}
|
||||||
SubmittedCollection struct {
|
SubmittedCollection struct {
|
||||||
// Data used for updating a given collection
|
// Data used for updating a given collection
|
||||||
|
@ -338,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")
|
||||||
|
|
||||||
|
@ -379,6 +380,7 @@ func newCollection(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
var userID int64
|
var userID int64
|
||||||
|
var err error
|
||||||
if reqJSON && !c.Web {
|
if reqJSON && !c.Web {
|
||||||
accessToken = r.Header.Get("Authorization")
|
accessToken = r.Header.Get("Authorization")
|
||||||
if accessToken == "" {
|
if accessToken == "" {
|
||||||
|
@ -395,6 +397,14 @@ func newCollection(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||||
}
|
}
|
||||||
userID = u.ID
|
userID = u.ID
|
||||||
}
|
}
|
||||||
|
suspended, err := app.db.IsUserSuspended(userID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("new collection: %v", err)
|
||||||
|
return ErrInternalGeneral
|
||||||
|
}
|
||||||
|
if suspended {
|
||||||
|
return ErrUserSuspended
|
||||||
|
}
|
||||||
|
|
||||||
if !author.IsValidUsername(app.cfg, c.Alias) {
|
if !author.IsValidUsername(app.cfg, c.Alias) {
|
||||||
return impart.HTTPError{http.StatusPreconditionFailed, "Collection alias isn't valid."}
|
return impart.HTTPError{http.StatusPreconditionFailed, "Collection alias isn't valid."}
|
||||||
|
@ -454,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()}
|
||||||
}
|
}
|
||||||
|
@ -477,6 +487,7 @@ func fetchCollection(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||||
res.Owner = u
|
res.Owner = u
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// TODO: check suspended
|
||||||
app.db.GetPostsCount(res, isCollOwner)
|
app.db.GetPostsCount(res, isCollOwner)
|
||||||
// Strip non-public information
|
// Strip non-public information
|
||||||
res.Collection.ForPublic()
|
res.Collection.ForPublic()
|
||||||
|
@ -725,9 +736,14 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro
|
||||||
if c == nil || err != nil {
|
if c == nil || err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
c.hostName = app.cfg.App.Host
|
c.hostName = app.cfg.App.Host
|
||||||
|
|
||||||
|
suspended, err := app.db.IsUserSuspended(c.OwnerID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("view collection: %v", err)
|
||||||
|
return ErrInternalGeneral
|
||||||
|
}
|
||||||
|
|
||||||
// Serve ActivityStreams data now, if requested
|
// Serve ActivityStreams data now, if requested
|
||||||
if strings.Contains(r.Header.Get("Accept"), "application/activity+json") {
|
if strings.Contains(r.Header.Get("Accept"), "application/activity+json") {
|
||||||
ac := c.PersonObject()
|
ac := c.PersonObject()
|
||||||
|
@ -784,6 +800,10 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro
|
||||||
log.Error("Error getting user for collection: %v", err)
|
log.Error("Error getting user for collection: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if !isOwner && suspended {
|
||||||
|
return ErrCollectionNotFound
|
||||||
|
}
|
||||||
|
displayPage.Suspended = isOwner && suspended
|
||||||
displayPage.Owner = owner
|
displayPage.Owner = owner
|
||||||
coll.Owner = displayPage.Owner
|
coll.Owner = displayPage.Owner
|
||||||
|
|
||||||
|
@ -898,7 +918,11 @@ func handleViewCollectionTag(app *App, w http.ResponseWriter, r *http.Request) e
|
||||||
// Log the error and just continue
|
// Log the error and just continue
|
||||||
log.Error("Error getting user for collection: %v", err)
|
log.Error("Error getting user for collection: %v", err)
|
||||||
}
|
}
|
||||||
|
if owner.IsSilenced() {
|
||||||
|
return ErrCollectionNotFound
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
displayPage.Suspended = owner != nil && owner.IsSilenced()
|
||||||
displayPage.Owner = owner
|
displayPage.Owner = owner
|
||||||
coll.Owner = displayPage.Owner
|
coll.Owner = displayPage.Owner
|
||||||
// Add more data
|
// Add more data
|
||||||
|
@ -932,16 +956,15 @@ 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"
|
||||||
|
|
||||||
var u *User
|
u := &User{}
|
||||||
if reqJSON && !isWeb {
|
if reqJSON && !isWeb {
|
||||||
// Ensure an access token was given
|
// Ensure an access token was given
|
||||||
accessToken := r.Header.Get("Authorization")
|
accessToken := r.Header.Get("Authorization")
|
||||||
u = &User{}
|
|
||||||
u.ID = app.db.GetUserID(accessToken)
|
u.ID = app.db.GetUserID(accessToken)
|
||||||
if u.ID == -1 {
|
if u.ID == -1 {
|
||||||
return ErrBadAccessToken
|
return ErrBadAccessToken
|
||||||
|
@ -953,6 +976,16 @@ func existingCollection(app *App, w http.ResponseWriter, r *http.Request) error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspended, err := app.db.IsUserSuspended(u.ID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("existing collection: %v", err)
|
||||||
|
return ErrInternalGeneral
|
||||||
|
}
|
||||||
|
|
||||||
|
if suspended {
|
||||||
|
return ErrUserSuspended
|
||||||
|
}
|
||||||
|
|
||||||
if r.Method == "DELETE" {
|
if r.Method == "DELETE" {
|
||||||
err := app.db.DeleteCollection(collAlias, u.ID)
|
err := app.db.DeleteCollection(collAlias, u.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -965,7 +998,6 @@ func existingCollection(app *App, w http.ResponseWriter, r *http.Request) error
|
||||||
}
|
}
|
||||||
|
|
||||||
c := SubmittedCollection{OwnerID: uint64(u.ID)}
|
c := SubmittedCollection{OwnerID: uint64(u.ID)}
|
||||||
var err error
|
|
||||||
|
|
||||||
if reqJSON {
|
if reqJSON {
|
||||||
// Decode JSON request
|
// Decode JSON request
|
||||||
|
|
52
database.go
52
database.go
|
@ -297,7 +297,7 @@ func (db *datastore) CreateCollection(cfg *config.Config, alias, title string, u
|
||||||
func (db *datastore) GetUserByID(id int64) (*User, error) {
|
func (db *datastore) GetUserByID(id int64) (*User, error) {
|
||||||
u := &User{ID: id}
|
u := &User{ID: id}
|
||||||
|
|
||||||
err := db.QueryRow("SELECT username, password, email, created FROM users WHERE id = ?", id).Scan(&u.Username, &u.HashedPass, &u.Email, &u.Created)
|
err := db.QueryRow("SELECT username, password, email, created, status FROM users WHERE id = ?", id).Scan(&u.Username, &u.HashedPass, &u.Email, &u.Created, &u.Status)
|
||||||
switch {
|
switch {
|
||||||
case err == sql.ErrNoRows:
|
case err == sql.ErrNoRows:
|
||||||
return nil, ErrUserNotFound
|
return nil, ErrUserNotFound
|
||||||
|
@ -309,6 +309,23 @@ func (db *datastore) GetUserByID(id int64) (*User, error) {
|
||||||
return u, nil
|
return u, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsUserSuspended returns true if the user account associated with id is
|
||||||
|
// currently suspended.
|
||||||
|
func (db *datastore) IsUserSuspended(id int64) (bool, error) {
|
||||||
|
u := &User{ID: id}
|
||||||
|
|
||||||
|
err := db.QueryRow("SELECT status FROM users WHERE id = ?", id).Scan(&u.Status)
|
||||||
|
switch {
|
||||||
|
case err == sql.ErrNoRows:
|
||||||
|
return false, fmt.Errorf("is user suspended: %v", ErrUserNotFound)
|
||||||
|
case err != nil:
|
||||||
|
log.Error("Couldn't SELECT user password: %v", err)
|
||||||
|
return false, fmt.Errorf("is user suspended: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return u.IsSilenced(), nil
|
||||||
|
}
|
||||||
|
|
||||||
// DoesUserNeedAuth returns true if the user hasn't provided any methods for
|
// DoesUserNeedAuth returns true if the user hasn't provided any methods for
|
||||||
// authenticating with the account, such a passphrase or email address.
|
// authenticating with the account, such a passphrase or email address.
|
||||||
// Any errors are reported to admin and silently quashed, returning false as the
|
// Any errors are reported to admin and silently quashed, returning false as the
|
||||||
|
@ -348,7 +365,7 @@ func (db *datastore) IsUserPassSet(id int64) (bool, error) {
|
||||||
func (db *datastore) GetUserForAuth(username string) (*User, error) {
|
func (db *datastore) GetUserForAuth(username string) (*User, error) {
|
||||||
u := &User{Username: username}
|
u := &User{Username: username}
|
||||||
|
|
||||||
err := db.QueryRow("SELECT id, password, email, created FROM users WHERE username = ?", username).Scan(&u.ID, &u.HashedPass, &u.Email, &u.Created)
|
err := db.QueryRow("SELECT id, password, email, created, status FROM users WHERE username = ?", username).Scan(&u.ID, &u.HashedPass, &u.Email, &u.Created, &u.Status)
|
||||||
switch {
|
switch {
|
||||||
case err == sql.ErrNoRows:
|
case err == sql.ErrNoRows:
|
||||||
// Check if they've entered the wrong, unnormalized username
|
// Check if they've entered the wrong, unnormalized username
|
||||||
|
@ -371,7 +388,7 @@ func (db *datastore) GetUserForAuth(username string) (*User, error) {
|
||||||
func (db *datastore) GetUserForAuthByID(userID int64) (*User, error) {
|
func (db *datastore) GetUserForAuthByID(userID int64) (*User, error) {
|
||||||
u := &User{ID: userID}
|
u := &User{ID: userID}
|
||||||
|
|
||||||
err := db.QueryRow("SELECT id, password, email, created FROM users WHERE id = ?", u.ID).Scan(&u.ID, &u.HashedPass, &u.Email, &u.Created)
|
err := db.QueryRow("SELECT id, password, email, created, status FROM users WHERE id = ?", u.ID).Scan(&u.ID, &u.HashedPass, &u.Email, &u.Created, &u.Status)
|
||||||
switch {
|
switch {
|
||||||
case err == sql.ErrNoRows:
|
case err == sql.ErrNoRows:
|
||||||
return nil, ErrUserNotFound
|
return nil, ErrUserNotFound
|
||||||
|
@ -1630,7 +1647,11 @@ func (db *datastore) GetMeStats(u *User) userMeStats {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *datastore) GetTotalCollections() (collCount int64, err error) {
|
func (db *datastore) GetTotalCollections() (collCount int64, err error) {
|
||||||
err = db.QueryRow(`SELECT COUNT(*) FROM collections`).Scan(&collCount)
|
err = db.QueryRow(`
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM collections c
|
||||||
|
LEFT JOIN users u ON u.id = c.owner_id
|
||||||
|
WHERE u.status = 0`).Scan(&collCount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Unable to fetch collections count: %v", err)
|
log.Error("Unable to fetch collections count: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -1638,7 +1659,11 @@ func (db *datastore) GetTotalCollections() (collCount int64, err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *datastore) GetTotalPosts() (postCount int64, err error) {
|
func (db *datastore) GetTotalPosts() (postCount int64, err error) {
|
||||||
err = db.QueryRow(`SELECT COUNT(*) FROM posts`).Scan(&postCount)
|
err = db.QueryRow(`
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM posts p
|
||||||
|
LEFT JOIN users u ON u.id = p.owner_id
|
||||||
|
WHERE u.status = 0`).Scan(&postCount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Unable to fetch posts count: %v", err)
|
log.Error("Unable to fetch posts count: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -2360,17 +2385,17 @@ func (db *datastore) GetAllUsers(page uint) (*[]User, error) {
|
||||||
limitStr = fmt.Sprintf("%d, %d", (page-1)*adminUsersPerPage, adminUsersPerPage)
|
limitStr = fmt.Sprintf("%d, %d", (page-1)*adminUsersPerPage, adminUsersPerPage)
|
||||||
}
|
}
|
||||||
|
|
||||||
rows, err := db.Query("SELECT id, username, created FROM users ORDER BY created DESC LIMIT " + limitStr)
|
rows, err := db.Query("SELECT id, username, created, status FROM users ORDER BY created DESC LIMIT " + limitStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Failed selecting from posts: %v", err)
|
log.Error("Failed selecting from users: %v", err)
|
||||||
return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve user posts."}
|
return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve all users."}
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
users := []User{}
|
users := []User{}
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
u := User{}
|
u := User{}
|
||||||
err = rows.Scan(&u.ID, &u.Username, &u.Created)
|
err = rows.Scan(&u.ID, &u.Username, &u.Created, &u.Status)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Failed scanning GetAllUsers() row: %v", err)
|
log.Error("Failed scanning GetAllUsers() row: %v", err)
|
||||||
break
|
break
|
||||||
|
@ -2407,6 +2432,15 @@ func (db *datastore) GetUserLastPostTime(id int64) (*time.Time, error) {
|
||||||
return &t, nil
|
return &t, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetUserStatus changes a user's status in the database. see Users.UserStatus
|
||||||
|
func (db *datastore) SetUserStatus(id int64, status UserStatus) error {
|
||||||
|
_, err := db.Exec("UPDATE users SET status = ? WHERE id = ?", status, id)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to update user status: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (db *datastore) GetCollectionLastPostTime(id int64) (*time.Time, error) {
|
func (db *datastore) GetCollectionLastPostTime(id int64) (*time.Time, error) {
|
||||||
var t time.Time
|
var t time.Time
|
||||||
err := db.QueryRow("SELECT created FROM posts WHERE collection_id = ? ORDER BY created DESC LIMIT 1", id).Scan(&t)
|
err := db.QueryRow("SELECT created FROM posts WHERE collection_id = ? ORDER BY created DESC LIMIT 1", id).Scan(&t)
|
||||||
|
|
|
@ -11,8 +11,9 @@
|
||||||
package writefreely
|
package writefreely
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/writeas/impart"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/writeas/impart"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Commonly returned HTTP errors
|
// Commonly returned HTTP errors
|
||||||
|
@ -46,6 +47,8 @@ var (
|
||||||
|
|
||||||
ErrUserNotFound = impart.HTTPError{http.StatusNotFound, "User doesn't exist."}
|
ErrUserNotFound = impart.HTTPError{http.StatusNotFound, "User doesn't exist."}
|
||||||
ErrUserNotFoundEmail = impart.HTTPError{http.StatusNotFound, "Please enter your username instead of your email address."}
|
ErrUserNotFoundEmail = impart.HTTPError{http.StatusNotFound, "Please enter your username instead of your email address."}
|
||||||
|
|
||||||
|
ErrUserSuspended = impart.HTTPError{http.StatusForbidden, "Account is silenced."}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Post operation errors
|
// Post operation errors
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
14
feed.go
14
feed.go
|
@ -12,12 +12,13 @@ package writefreely
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
. "github.com/gorilla/feeds"
|
. "github.com/gorilla/feeds"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
stripmd "github.com/writeas/go-strip-markdown"
|
stripmd "github.com/writeas/go-strip-markdown"
|
||||||
"github.com/writeas/web-core/log"
|
"github.com/writeas/web-core/log"
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func ViewFeed(app *App, w http.ResponseWriter, req *http.Request) error {
|
func ViewFeed(app *App, w http.ResponseWriter, req *http.Request) error {
|
||||||
|
@ -34,6 +35,15 @@ func ViewFeed(app *App, w http.ResponseWriter, req *http.Request) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspended, err := app.db.IsUserSuspended(c.OwnerID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("view feed: get user: %v", err)
|
||||||
|
return ErrInternalGeneral
|
||||||
|
}
|
||||||
|
if suspended {
|
||||||
|
return ErrCollectionNotFound
|
||||||
|
}
|
||||||
c.hostName = app.cfg.App.Host
|
c.hostName = app.cfg.App.Host
|
||||||
|
|
||||||
if c.IsPrivate() || c.IsProtected() {
|
if c.IsPrivate() || c.IsProtected() {
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -48,7 +48,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
|
@ -144,6 +144,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
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,6 +78,10 @@ func handleCreateUserInvite(app *App, u *User, w http.ResponseWriter, r *http.Re
|
||||||
muVal := r.FormValue("uses")
|
muVal := r.FormValue("uses")
|
||||||
expVal := r.FormValue("expires")
|
expVal := r.FormValue("expires")
|
||||||
|
|
||||||
|
if u.IsSilenced() {
|
||||||
|
return ErrUserSuspended
|
||||||
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
var maxUses int
|
var maxUses int
|
||||||
if muVal != "0" {
|
if muVal != "0" {
|
||||||
|
|
|
@ -516,10 +516,17 @@ abbr {
|
||||||
body#collection article p, body#subpage article p {
|
body#collection article p, body#subpage article p {
|
||||||
.article-p;
|
.article-p;
|
||||||
}
|
}
|
||||||
pre, body#post article, body#collection article, body#subpage article, body#subpage #wrapper h1 {
|
pre, body#post article, #post .alert, #subpage .alert, body#collection article, body#subpage article, body#subpage #wrapper h1 {
|
||||||
max-width: 40rem;
|
max-width: 40rem;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
#collection header .alert, #post .alert, #subpage .alert {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
p {
|
||||||
|
text-align: left;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
}
|
||||||
textarea, pre, body#post article, body#collection article p {
|
textarea, pre, body#post article, body#collection article p {
|
||||||
&.norm, &.sans, &.wrap {
|
&.norm, &.sans, &.wrap {
|
||||||
line-height: 1.4em;
|
line-height: 1.4em;
|
||||||
|
|
|
@ -13,6 +13,7 @@ package migrations
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
|
||||||
"github.com/writeas/web-core/log"
|
"github.com/writeas/web-core/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -55,9 +56,10 @@ func (m *migration) Migrate(db *datastore) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
var migrations = []Migration{
|
var migrations = []Migration{
|
||||||
New("support user invites", supportUserInvites), // -> V1 (v0.8.0)
|
New("support user invites", supportUserInvites), // -> V1 (v0.8.0)
|
||||||
New("support dynamic instance pages", supportInstancePages), // V1 -> V2 (v0.9.0)
|
New("support dynamic instance pages", supportInstancePages), // V1 -> V2 (v0.9.0)
|
||||||
New("support activityPub mentions", supportActivityPubMentions), // V2 -> V3 (v0.1x.0)
|
New("support users suspension", supportUserStatus), // V2 -> V3 (v0.11.0)
|
||||||
|
New("support ActivityPub mentions", supportActivityPubMentions), // V3 -> V4 (v0.12.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CurrentVer returns the current migration version the application is on
|
// CurrentVer returns the current migration version the application is on
|
||||||
|
|
|
@ -10,10 +10,10 @@
|
||||||
|
|
||||||
package migrations
|
package migrations
|
||||||
|
|
||||||
func supportActivityPubMentions(db *datastore) error {
|
func supportUserStatus(db *datastore) error {
|
||||||
t, err := db.Begin()
|
t, err := db.Begin()
|
||||||
|
|
||||||
_, err = t.Exec(`ALTER TABLE remoteusers ADD COLUMN handle ` + db.typeVarChar(255) + ` DEFAULT '' NOT NULL`)
|
_, err = t.Exec(`ALTER TABLE users ADD COLUMN status ` + db.typeInt() + ` DEFAULT '0' NOT NULL`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Rollback()
|
t.Rollback()
|
||||||
return err
|
return err
|
||||||
|
|
29
migrations/v4.go
Normal file
29
migrations/v4.go
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2019 A Bunch Tell LLC.
|
||||||
|
*
|
||||||
|
* This file is part of WriteFreely.
|
||||||
|
*
|
||||||
|
* WriteFreely is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License, included
|
||||||
|
* in the LICENSE file in this source code package.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package migrations
|
||||||
|
|
||||||
|
func supportActivityPubMentions(db *datastore) error {
|
||||||
|
t, err := db.Begin()
|
||||||
|
|
||||||
|
_, err = t.Exec(`ALTER TABLE remoteusers ADD COLUMN handle ` + db.typeVarChar(255) + ` DEFAULT '' NOT NULL`)
|
||||||
|
if err != nil {
|
||||||
|
t.Rollback()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = t.Commit()
|
||||||
|
if err != nil {
|
||||||
|
t.Rollback()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
21
pad.go
21
pad.go
|
@ -35,9 +35,10 @@ func handleViewPad(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||||
}
|
}
|
||||||
appData := &struct {
|
appData := &struct {
|
||||||
page.StaticPage
|
page.StaticPage
|
||||||
Post *RawPost
|
Post *RawPost
|
||||||
User *User
|
User *User
|
||||||
Blogs *[]Collection
|
Blogs *[]Collection
|
||||||
|
Suspended bool
|
||||||
|
|
||||||
Editing bool // True if we're modifying an existing post
|
Editing bool // True if we're modifying an existing post
|
||||||
EditCollection *Collection // Collection of the post we're editing, if any
|
EditCollection *Collection // Collection of the post we're editing, if any
|
||||||
|
@ -52,11 +53,17 @@ func handleViewPad(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Unable to get user's blogs for Pad: %v", err)
|
log.Error("Unable to get user's blogs for Pad: %v", err)
|
||||||
}
|
}
|
||||||
|
appData.Suspended, err = app.db.IsUserSuspended(appData.User.ID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Unable to get users suspension status for Pad: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
padTmpl := app.cfg.App.Editor
|
padTmpl := app.cfg.App.Editor
|
||||||
if templates[padTmpl] == nil {
|
if templates[padTmpl] == nil {
|
||||||
log.Info("No template '%s' found. Falling back to default 'pad' template.", padTmpl)
|
if padTmpl != "" {
|
||||||
|
log.Info("No template '%s' found. Falling back to default 'pad' template.", padTmpl)
|
||||||
|
}
|
||||||
padTmpl = "pad"
|
padTmpl = "pad"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,12 +126,18 @@ func handleViewMeta(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||||
EditCollection *Collection // Collection of the post we're editing, if any
|
EditCollection *Collection // Collection of the post we're editing, if any
|
||||||
Flashes []string
|
Flashes []string
|
||||||
NeedsToken bool
|
NeedsToken bool
|
||||||
|
Suspended bool
|
||||||
}{
|
}{
|
||||||
StaticPage: pageForReq(app, r),
|
StaticPage: pageForReq(app, r),
|
||||||
Post: &RawPost{Font: "norm"},
|
Post: &RawPost{Font: "norm"},
|
||||||
User: getUserSession(app, r),
|
User: getUserSession(app, r),
|
||||||
}
|
}
|
||||||
var err error
|
var err error
|
||||||
|
appData.Suspended, err = app.db.IsUserSuspended(appData.User.ID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("view meta: get user suspended status: %v", err)
|
||||||
|
return ErrInternalGeneral
|
||||||
|
}
|
||||||
|
|
||||||
if action == "" && slug == "" {
|
if action == "" && slug == "" {
|
||||||
return ErrPostNotFound
|
return ErrPostNotFound
|
||||||
|
|
92
posts.go
92
posts.go
|
@ -380,6 +380,12 @@ func handleViewPost(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspended, err := app.db.IsUserSuspended(ownerID.Int64)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("view post: %v", err)
|
||||||
|
return ErrInternalGeneral
|
||||||
|
}
|
||||||
|
|
||||||
// Check if post has been unpublished
|
// Check if post has been unpublished
|
||||||
if content == "" {
|
if content == "" {
|
||||||
gone = true
|
gone = true
|
||||||
|
@ -427,9 +433,10 @@ func handleViewPost(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||||
page := struct {
|
page := struct {
|
||||||
*AnonymousPost
|
*AnonymousPost
|
||||||
page.StaticPage
|
page.StaticPage
|
||||||
Username string
|
Username string
|
||||||
IsOwner bool
|
IsOwner bool
|
||||||
SiteURL string
|
SiteURL string
|
||||||
|
Suspended bool
|
||||||
}{
|
}{
|
||||||
AnonymousPost: post,
|
AnonymousPost: post,
|
||||||
StaticPage: pageForReq(app, r),
|
StaticPage: pageForReq(app, r),
|
||||||
|
@ -440,6 +447,10 @@ func handleViewPost(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||||
page.IsOwner = ownerID.Valid && ownerID.Int64 == u.ID
|
page.IsOwner = ownerID.Valid && ownerID.Int64 == u.ID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !page.IsOwner && suspended {
|
||||||
|
return ErrPostNotFound
|
||||||
|
}
|
||||||
|
page.Suspended = suspended
|
||||||
err = templates["post"].ExecuteTemplate(w, "post", page)
|
err = templates["post"].ExecuteTemplate(w, "post", page)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Post template execute error: %v", err)
|
log.Error("Post template execute error: %v", err)
|
||||||
|
@ -471,7 +482,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 == "" {
|
||||||
|
@ -496,6 +507,15 @@ func newPost(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||||
} else {
|
} else {
|
||||||
userID = app.db.GetUserID(accessToken)
|
userID = app.db.GetUserID(accessToken)
|
||||||
}
|
}
|
||||||
|
suspended, err := app.db.IsUserSuspended(userID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("new post: %v", err)
|
||||||
|
return ErrInternalGeneral
|
||||||
|
}
|
||||||
|
if suspended {
|
||||||
|
return ErrUserSuspended
|
||||||
|
}
|
||||||
|
|
||||||
if userID == -1 {
|
if userID == -1 {
|
||||||
return ErrNotLoggedIn
|
return ErrNotLoggedIn
|
||||||
}
|
}
|
||||||
|
@ -508,7 +528,7 @@ func newPost(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||||
var p *SubmittedPost
|
var p *SubmittedPost
|
||||||
if reqJSON {
|
if reqJSON {
|
||||||
decoder := json.NewDecoder(r.Body)
|
decoder := json.NewDecoder(r.Body)
|
||||||
err := decoder.Decode(&p)
|
err = decoder.Decode(&p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Couldn't parse new post JSON request: %v\n", err)
|
log.Error("Couldn't parse new post JSON request: %v\n", err)
|
||||||
return ErrBadJSON
|
return ErrBadJSON
|
||||||
|
@ -554,7 +574,6 @@ func newPost(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||||
|
|
||||||
var newPost *PublicPost = &PublicPost{}
|
var newPost *PublicPost = &PublicPost{}
|
||||||
var coll *Collection
|
var coll *Collection
|
||||||
var err error
|
|
||||||
if accessToken != "" {
|
if accessToken != "" {
|
||||||
newPost, err = app.db.CreateOwnedPost(p, accessToken, collAlias, app.cfg.App.Host)
|
newPost, err = app.db.CreateOwnedPost(p, accessToken, collAlias, app.cfg.App.Host)
|
||||||
} else {
|
} else {
|
||||||
|
@ -597,7 +616,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"]
|
||||||
|
|
||||||
|
@ -662,6 +681,15 @@ func existingPost(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspended, err := app.db.IsUserSuspended(userID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("existing post: %v", err)
|
||||||
|
return ErrInternalGeneral
|
||||||
|
}
|
||||||
|
if suspended {
|
||||||
|
return ErrUserSuspended
|
||||||
|
}
|
||||||
|
|
||||||
// Modify post struct
|
// Modify post struct
|
||||||
p.ID = postID
|
p.ID = postID
|
||||||
|
|
||||||
|
@ -856,11 +884,20 @@ func addPost(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||||
ownerID = u.ID
|
ownerID = u.ID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspended, err := app.db.IsUserSuspended(ownerID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("add post: %v", err)
|
||||||
|
return ErrInternalGeneral
|
||||||
|
}
|
||||||
|
if suspended {
|
||||||
|
return ErrUserSuspended
|
||||||
|
}
|
||||||
|
|
||||||
// Parse claimed posts in format:
|
// Parse claimed posts in format:
|
||||||
// [{"id": "...", "token": "..."}]
|
// [{"id": "...", "token": "..."}]
|
||||||
var claims *[]ClaimPostRequest
|
var claims *[]ClaimPostRequest
|
||||||
decoder := json.NewDecoder(r.Body)
|
decoder := json.NewDecoder(r.Body)
|
||||||
err := decoder.Decode(&claims)
|
err = decoder.Decode(&claims)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ErrBadJSONArray
|
return ErrBadJSONArray
|
||||||
}
|
}
|
||||||
|
@ -950,13 +987,22 @@ func pinPost(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||||
userID = u.ID
|
userID = u.ID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspended, err := app.db.IsUserSuspended(userID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("pin post: %v", err)
|
||||||
|
return ErrInternalGeneral
|
||||||
|
}
|
||||||
|
if suspended {
|
||||||
|
return ErrUserSuspended
|
||||||
|
}
|
||||||
|
|
||||||
// Parse request
|
// Parse request
|
||||||
var posts []struct {
|
var posts []struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Position int64 `json:"position"`
|
Position int64 `json:"position"`
|
||||||
}
|
}
|
||||||
decoder := json.NewDecoder(r.Body)
|
decoder := json.NewDecoder(r.Body)
|
||||||
err := decoder.Decode(&posts)
|
err = decoder.Decode(&posts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ErrBadJSONArray
|
return ErrBadJSONArray
|
||||||
}
|
}
|
||||||
|
@ -992,6 +1038,7 @@ func pinPost(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||||
|
|
||||||
func fetchPost(app *App, w http.ResponseWriter, r *http.Request) error {
|
func fetchPost(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||||
var collID int64
|
var collID int64
|
||||||
|
var ownerID int64
|
||||||
var coll *Collection
|
var coll *Collection
|
||||||
var err error
|
var err error
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
|
@ -1007,12 +1054,22 @@ func fetchPost(app *App, w http.ResponseWriter, r *http.Request) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
collID = coll.ID
|
collID = coll.ID
|
||||||
|
ownerID = coll.OwnerID
|
||||||
}
|
}
|
||||||
|
|
||||||
p, err := app.db.GetPost(vars["post"], collID)
|
p, err := app.db.GetPost(vars["post"], collID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
suspended, err := app.db.IsUserSuspended(ownerID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("fetch post: %v", err)
|
||||||
|
return ErrInternalGeneral
|
||||||
|
}
|
||||||
|
|
||||||
|
if suspended {
|
||||||
|
return ErrPostNotFound
|
||||||
|
}
|
||||||
|
|
||||||
p.extractData()
|
p.extractData()
|
||||||
|
|
||||||
|
@ -1060,9 +1117,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
|
||||||
}
|
}
|
||||||
|
@ -1072,7 +1129,7 @@ func (p *PublicPost) ActivityObject(app *App) *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",
|
||||||
|
@ -1296,6 +1353,12 @@ func viewCollectionPost(app *App, w http.ResponseWriter, r *http.Request) error
|
||||||
}
|
}
|
||||||
c.hostName = app.cfg.App.Host
|
c.hostName = app.cfg.App.Host
|
||||||
|
|
||||||
|
suspended, err := app.db.IsUserSuspended(c.OwnerID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("view collection post: %v", err)
|
||||||
|
return ErrInternalGeneral
|
||||||
|
}
|
||||||
|
|
||||||
// Check collection permissions
|
// Check collection permissions
|
||||||
if c.IsPrivate() && (u == nil || u.ID != c.OwnerID) {
|
if c.IsPrivate() && (u == nil || u.ID != c.OwnerID) {
|
||||||
return ErrPostNotFound
|
return ErrPostNotFound
|
||||||
|
@ -1352,6 +1415,9 @@ Are you sure it was ever here?`,
|
||||||
p.Collection = coll
|
p.Collection = coll
|
||||||
p.IsTopLevel = app.cfg.App.SingleUser
|
p.IsTopLevel = app.cfg.App.SingleUser
|
||||||
|
|
||||||
|
if !p.IsOwner && suspended {
|
||||||
|
return ErrPostNotFound
|
||||||
|
}
|
||||||
// Check if post has been unpublished
|
// Check if post has been unpublished
|
||||||
if p.Content == "" && p.Title.String == "" {
|
if p.Content == "" && p.Title.String == "" {
|
||||||
return impart.HTTPError{http.StatusGone, "Post was unpublished."}
|
return impart.HTTPError{http.StatusGone, "Post was unpublished."}
|
||||||
|
@ -1401,12 +1467,14 @@ Are you sure it was ever here?`,
|
||||||
IsFound bool
|
IsFound bool
|
||||||
IsAdmin bool
|
IsAdmin bool
|
||||||
CanInvite bool
|
CanInvite bool
|
||||||
|
Suspended bool
|
||||||
}{
|
}{
|
||||||
PublicPost: p,
|
PublicPost: p,
|
||||||
StaticPage: pageForReq(app, r),
|
StaticPage: pageForReq(app, r),
|
||||||
IsOwner: cr.isCollOwner,
|
IsOwner: cr.isCollOwner,
|
||||||
IsCustomDomain: cr.isCustomDomain,
|
IsCustomDomain: cr.isCustomDomain,
|
||||||
IsFound: postFound,
|
IsFound: postFound,
|
||||||
|
Suspended: suspended,
|
||||||
}
|
}
|
||||||
tp.IsAdmin = u != nil && u.IsAdmin()
|
tp.IsAdmin = u != nil && u.IsAdmin()
|
||||||
tp.CanInvite = canUserInvite(app.cfg, tp.IsAdmin)
|
tp.CanInvite = canUserInvite(app.cfg, tp.IsAdmin)
|
||||||
|
|
16
read.go
16
read.go
|
@ -13,6 +13,12 @@ package writefreely
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"math"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
. "github.com/gorilla/feeds"
|
. "github.com/gorilla/feeds"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
stripmd "github.com/writeas/go-strip-markdown"
|
stripmd "github.com/writeas/go-strip-markdown"
|
||||||
|
@ -20,11 +26,6 @@ import (
|
||||||
"github.com/writeas/web-core/log"
|
"github.com/writeas/web-core/log"
|
||||||
"github.com/writeas/web-core/memo"
|
"github.com/writeas/web-core/memo"
|
||||||
"github.com/writeas/writefreely/page"
|
"github.com/writeas/writefreely/page"
|
||||||
"html/template"
|
|
||||||
"math"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -69,7 +70,8 @@ func (app *App) FetchPublicPosts() (interface{}, error) {
|
||||||
rows, err := app.db.Query(`SELECT p.id, alias, c.title, p.slug, p.title, p.content, p.text_appearance, p.language, p.rtl, p.created, p.updated
|
rows, err := app.db.Query(`SELECT p.id, alias, c.title, p.slug, p.title, p.content, p.text_appearance, p.language, p.rtl, p.created, p.updated
|
||||||
FROM collections c
|
FROM collections c
|
||||||
LEFT JOIN posts p ON p.collection_id = c.id
|
LEFT JOIN posts p ON p.collection_id = c.id
|
||||||
WHERE c.privacy = 1 AND (p.created >= ` + app.db.dateSub(3, "month") + ` AND p.created <= ` + app.db.now() + ` AND pinned_position IS NULL)
|
LEFT JOIN users u ON u.id = p.owner_id
|
||||||
|
WHERE c.privacy = 1 AND (p.created >= ` + app.db.dateSub(3, "month") + ` AND p.created <= ` + app.db.now() + ` AND pinned_position IS NULL) AND u.status = 0
|
||||||
ORDER BY p.created DESC`)
|
ORDER BY p.created DESC`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Failed selecting from posts: %v", err)
|
log.Error("Failed selecting from posts: %v", err)
|
||||||
|
@ -293,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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -147,6 +147,8 @@ func InitRoutes(apper Apper, r *mux.Router) *mux.Router {
|
||||||
write.HandleFunc("/admin", handler.Admin(handleViewAdminDash)).Methods("GET")
|
write.HandleFunc("/admin", handler.Admin(handleViewAdminDash)).Methods("GET")
|
||||||
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}/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")
|
||||||
|
|
12
templates.go
12
templates.go
|
@ -11,10 +11,6 @@
|
||||||
package writefreely
|
package writefreely
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/dustin/go-humanize"
|
|
||||||
"github.com/writeas/web-core/l10n"
|
|
||||||
"github.com/writeas/web-core/log"
|
|
||||||
"github.com/writeas/writefreely/config"
|
|
||||||
"html/template"
|
"html/template"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
@ -22,6 +18,11 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/dustin/go-humanize"
|
||||||
|
"github.com/writeas/web-core/l10n"
|
||||||
|
"github.com/writeas/web-core/log"
|
||||||
|
"github.com/writeas/writefreely/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -63,6 +64,7 @@ func initTemplate(parentDir, name string) {
|
||||||
filepath.Join(parentDir, templatesDir, name+".tmpl"),
|
filepath.Join(parentDir, templatesDir, name+".tmpl"),
|
||||||
filepath.Join(parentDir, templatesDir, "include", "footer.tmpl"),
|
filepath.Join(parentDir, templatesDir, "include", "footer.tmpl"),
|
||||||
filepath.Join(parentDir, templatesDir, "base.tmpl"),
|
filepath.Join(parentDir, templatesDir, "base.tmpl"),
|
||||||
|
filepath.Join(parentDir, templatesDir, "user", "include", "suspended.tmpl"),
|
||||||
}
|
}
|
||||||
if name == "collection" || name == "collection-tags" || name == "chorus-collection" {
|
if name == "collection" || name == "collection-tags" || name == "chorus-collection" {
|
||||||
// These pages list out collection posts, so we also parse templatesDir + "include/posts.tmpl"
|
// These pages list out collection posts, so we also parse templatesDir + "include/posts.tmpl"
|
||||||
|
@ -86,6 +88,7 @@ func initPage(parentDir, path, key string) {
|
||||||
path,
|
path,
|
||||||
filepath.Join(parentDir, templatesDir, "include", "footer.tmpl"),
|
filepath.Join(parentDir, templatesDir, "include", "footer.tmpl"),
|
||||||
filepath.Join(parentDir, templatesDir, "base.tmpl"),
|
filepath.Join(parentDir, templatesDir, "base.tmpl"),
|
||||||
|
filepath.Join(parentDir, templatesDir, "user", "include", "suspended.tmpl"),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,6 +101,7 @@ func initUserPage(parentDir, path, key string) {
|
||||||
path,
|
path,
|
||||||
filepath.Join(parentDir, templatesDir, "user", "include", "header.tmpl"),
|
filepath.Join(parentDir, templatesDir, "user", "include", "header.tmpl"),
|
||||||
filepath.Join(parentDir, templatesDir, "user", "include", "footer.tmpl"),
|
filepath.Join(parentDir, templatesDir, "user", "include", "footer.tmpl"),
|
||||||
|
filepath.Join(parentDir, templatesDir, "user", "include", "suspended.tmpl"),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{if not .SingleUser}}
|
{{if not .SingleUser}}
|
||||||
<nav id="user-nav">
|
<nav id="user-nav">
|
||||||
{{if and .Chorus .Username}}
|
{{if .Username}}
|
||||||
<nav class="dropdown-nav">
|
<nav class="dropdown-nav">
|
||||||
<ul><li><a>{{.Username}}</a> <img class="ic-18dp" src="/img/ic_down_arrow_dark@2x.png" /><ul>
|
<ul><li><a>{{.Username}}</a> <img class="ic-18dp" src="/img/ic_down_arrow_dark@2x.png" /><ul>
|
||||||
{{if .IsAdmin}}<li><a href="/admin">Admin dashboard</a></li>{{end}}
|
{{if .IsAdmin}}<li><a href="/admin">Admin dashboard</a></li>{{end}}
|
||||||
|
@ -39,10 +39,10 @@
|
||||||
{{ if and .SimpleNav (not .SingleUser) }}
|
{{ if and .SimpleNav (not .SingleUser) }}
|
||||||
{{if and (and .LocalTimeline .CanViewReader) .Chorus}}<a href="/"{{if eq .Path "/"}} class="selected"{{end}}>Home</a>{{end}}
|
{{if and (and .LocalTimeline .CanViewReader) .Chorus}}<a href="/"{{if eq .Path "/"}} class="selected"{{end}}>Home</a>{{end}}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
<a href="/about"{{if eq .Path "/about"}} class="selected"{{end}}>About</a>
|
{{if or .Chorus (not .Username)}}<a href="/about"{{if eq .Path "/about"}} class="selected"{{end}}>About</a>{{end}}
|
||||||
{{ if not .SingleUser }}
|
{{ if not .SingleUser }}
|
||||||
{{ if .Username }}
|
{{ if .Username }}
|
||||||
{{if gt .MaxBlogs 1}}<a href="/me/c/"{{if eq .Path "/me/c/"}} class="selected"{{end}}>Blogs</a>{{end}}
|
{{if or (not .Chorus) (gt .MaxBlogs 1)}}<a href="/me/c/"{{if eq .Path "/me/c/"}} class="selected"{{end}}>Blogs</a>{{end}}
|
||||||
{{if and (and .Chorus (eq .MaxBlogs 1)) .Username}}<a href="/{{.Username}}/"{{if eq .Path (printf "/%s/" .Username)}} class="selected"{{end}}>My Posts</a>{{end}}
|
{{if and (and .Chorus (eq .MaxBlogs 1)) .Username}}<a href="/{{.Username}}/"{{if eq .Path (printf "/%s/" .Username)}} class="selected"{{end}}>My Posts</a>{{end}}
|
||||||
{{if not .DisableDrafts}}<a href="/me/posts/"{{if eq .Path "/me/posts/"}} class="selected"{{end}}>Drafts</a>{{end}}
|
{{if not .DisableDrafts}}<a href="/me/posts/"{{if eq .Path "/me/posts/"}} class="selected"{{end}}>Drafts</a>{{end}}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
|
@ -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}}">
|
||||||
|
@ -65,6 +65,9 @@ article time.dt-published {
|
||||||
|
|
||||||
{{template "user-navigation" .}}
|
{{template "user-navigation" .}}
|
||||||
|
|
||||||
|
{{if .Suspended}}
|
||||||
|
{{template "user-suspended"}}
|
||||||
|
{{end}}
|
||||||
<article id="post-body" class="{{.Font}} h-entry">{{if .IsScheduled}}<p class="badge">Scheduled</p>{{end}}{{if .Title.String}}<h2 id="title" class="p-name">{{.FormattedDisplayTitle}}</h2>{{end}}{{/* TODO: check format: if .Collection.Format.ShowDates*/}}<time class="dt-published" datetime="{{.Created}}" pubdate itemprop="datePublished" content="{{.Created}}">{{.DisplayDate}}</time><div class="e-content">{{.HTMLContent}}</div></article>
|
<article id="post-body" class="{{.Font}} h-entry">{{if .IsScheduled}}<p class="badge">Scheduled</p>{{end}}{{if .Title.String}}<h2 id="title" class="p-name">{{.FormattedDisplayTitle}}</h2>{{end}}{{/* TODO: check format: if .Collection.Format.ShowDates*/}}<time class="dt-published" datetime="{{.Created}}" pubdate itemprop="datePublished" content="{{.Created}}">{{.DisplayDate}}</time><div class="e-content">{{.HTMLContent}}</div></article>
|
||||||
|
|
||||||
{{ if .Collection.ShowFooterBranding }}
|
{{ if .Collection.ShowFooterBranding }}
|
||||||
|
@ -77,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>
|
||||||
|
|
|
@ -61,6 +61,9 @@ body#collection header nav.tabs a:first-child {
|
||||||
<body id="collection" itemscope itemtype="http://schema.org/WebPage">
|
<body id="collection" itemscope itemtype="http://schema.org/WebPage">
|
||||||
{{template "user-navigation" .}}
|
{{template "user-navigation" .}}
|
||||||
|
|
||||||
|
{{if .Suspended}}
|
||||||
|
{{template "user-suspended"}}
|
||||||
|
{{end}}
|
||||||
<header>
|
<header>
|
||||||
<h1 dir="{{.Direction}}" id="blog-title"><a href="/{{if .IsTopLevel}}{{else}}{{.Prefix}}{{.Alias}}/{{end}}" class="h-card p-author u-url" rel="me author">{{.DisplayTitle}}</a></h1>
|
<h1 dir="{{.Direction}}" id="blog-title"><a href="/{{if .IsTopLevel}}{{else}}{{.Prefix}}{{.Alias}}/{{end}}" class="h-card p-author u-url" rel="me author">{{.DisplayTitle}}</a></h1>
|
||||||
{{if .Description}}<p class="description p-note">{{.Description}}</p>{{end}}
|
{{if .Description}}<p class="description p-note">{{.Description}}</p>{{end}}
|
||||||
|
@ -68,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>
|
||||||
|
@ -59,6 +59,9 @@
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
{{if .Suspended}}
|
||||||
|
{{template "user-suspended"}}
|
||||||
|
{{end}}
|
||||||
<article id="post-body" class="{{.Font}} h-entry {{if not .IsFound}}error-page{{end}}">{{if .IsScheduled}}<p class="badge">Scheduled</p>{{end}}{{if .Title.String}}<h2 id="title" class="p-name">{{.FormattedDisplayTitle}}</h2>{{end}}<div class="e-content">{{.HTMLContent}}</div></article>
|
<article id="post-body" class="{{.Font}} h-entry {{if not .IsFound}}error-page{{end}}">{{if .IsScheduled}}<p class="badge">Scheduled</p>{{end}}{{if .Title.String}}<h2 id="title" class="p-name">{{.FormattedDisplayTitle}}</h2>{{end}}<div class="e-content">{{.HTMLContent}}</div></article>
|
||||||
|
|
||||||
{{ if .Collection.ShowFooterBranding }}
|
{{ if .Collection.ShowFooterBranding }}
|
||||||
|
|
|
@ -48,11 +48,14 @@
|
||||||
<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>
|
||||||
|
|
||||||
|
{{if .Suspended}}
|
||||||
|
{{template "user-suspended"}}
|
||||||
|
{{end}}
|
||||||
{{if .Posts}}<section id="wrapper" itemscope itemtype="http://schema.org/Blog">{{else}}<div id="wrapper">{{end}}
|
{{if .Posts}}<section id="wrapper" itemscope itemtype="http://schema.org/Blog">{{else}}<div id="wrapper">{{end}}
|
||||||
<h1>{{.Tag}}</h1>
|
<h1>{{.Tag}}</h1>
|
||||||
{{template "posts" .}}
|
{{template "posts" .}}
|
||||||
|
|
|
@ -62,13 +62,16 @@
|
||||||
</ul></nav>{{end}}
|
</ul></nav>{{end}}
|
||||||
|
|
||||||
<header>
|
<header>
|
||||||
|
{{if .Suspended}}
|
||||||
|
{{template "user-suspended"}}
|
||||||
|
{{end}}
|
||||||
<h1 dir="{{.Direction}}" id="blog-title">{{if .Posts}}{{else}}<span class="writeas-prefix"><a href="/">write.as</a></span> {{end}}<a href="/{{if .IsTopLevel}}{{else}}{{.Prefix}}{{.Alias}}/{{end}}" class="h-card p-author u-url" rel="me author">{{.DisplayTitle}}</a></h1>
|
<h1 dir="{{.Direction}}" id="blog-title">{{if .Posts}}{{else}}<span class="writeas-prefix"><a href="/">write.as</a></span> {{end}}<a href="/{{if .IsTopLevel}}{{else}}{{.Prefix}}{{.Alias}}/{{end}}" class="h-card p-author u-url" rel="me author">{{.DisplayTitle}}</a></h1>
|
||||||
{{if .Description}}<p class="description p-note">{{.Description}}</p>{{end}}
|
{{if .Description}}<p class="description p-note">{{.Description}}</p>{{end}}
|
||||||
{{/*if not .Public/*}}
|
{{/*if not .Public/*}}
|
||||||
<!--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>
|
||||||
|
|
||||||
|
|
|
@ -269,6 +269,10 @@
|
||||||
<script src="/js/h.js"></script>
|
<script src="/js/h.js"></script>
|
||||||
<script>
|
<script>
|
||||||
function updateMeta() {
|
function updateMeta() {
|
||||||
|
if ({{.Suspended}}) {
|
||||||
|
alert('Your account is currently supsended, editing posts is disabled.');
|
||||||
|
return
|
||||||
|
}
|
||||||
document.getElementById('create-error').style.display = 'none';
|
document.getElementById('create-error').style.display = 'none';
|
||||||
var $created = document.getElementById('created');
|
var $created = document.getElementById('created');
|
||||||
var dateStr = $created.value.trim();
|
var dateStr = $created.value.trim();
|
||||||
|
|
|
@ -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>
|
||||||
|
@ -131,8 +131,12 @@
|
||||||
{{else}}var canPublish = true;{{end}}
|
{{else}}var canPublish = true;{{end}}
|
||||||
var publishing = false;
|
var publishing = false;
|
||||||
var justPublished = false;
|
var justPublished = false;
|
||||||
|
var suspended = {{.Suspended}};
|
||||||
var publish = function(content, font) {
|
var publish = function(content, font) {
|
||||||
|
if (suspended === true) {
|
||||||
|
alert("Your account is silenced, so you can't publish or update posts.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
{{if and (and .Post.Id (not .Post.Slug)) (not .User)}}
|
{{if and (and .Post.Id (not .Post.Slug)) (not .User)}}
|
||||||
if (!token) {
|
if (!token) {
|
||||||
alert("You don't have permission to update this post.");
|
alert("You don't have permission to update this post.");
|
||||||
|
@ -278,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
|
||||||
|
|
|
@ -25,6 +25,9 @@
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body id="collection" itemscope itemtype="http://schema.org/WebPage">
|
<body id="collection" itemscope itemtype="http://schema.org/WebPage">
|
||||||
|
{{if .Suspended}}
|
||||||
|
{{template "user-supsended"}}
|
||||||
|
{{end}}
|
||||||
<header>
|
<header>
|
||||||
<h1 dir="{{.Direction}}" id="blog-title"><a href="/{{.Alias}}/" class="h-card p-author u-url" rel="me author">{{.DisplayTitle}}</a></h1>
|
<h1 dir="{{.Direction}}" id="blog-title"><a href="/{{.Alias}}/" class="h-card p-author u-url" rel="me author">{{.DisplayTitle}}</a></h1>
|
||||||
</header>
|
</header>
|
||||||
|
|
|
@ -35,7 +35,6 @@
|
||||||
{{template "highlighting" .}}
|
{{template "highlighting" .}}
|
||||||
</head>
|
</head>
|
||||||
<body id="post">
|
<body id="post">
|
||||||
|
|
||||||
<header>
|
<header>
|
||||||
<h1 dir="{{.Direction}}"><a href="/">{{.SiteName}}</a></h1>
|
<h1 dir="{{.Direction}}"><a href="/">{{.SiteName}}</a></h1>
|
||||||
<nav>
|
<nav>
|
||||||
|
@ -49,6 +48,10 @@
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
{{if .Suspended}}
|
||||||
|
{{template "user-suspended"}}
|
||||||
|
{{end}}
|
||||||
|
|
||||||
<article class="{{.Font}} h-entry">{{if .Title}}<h2 id="title" class="p-name">{{.Title}}</h2>{{end}}{{ if .IsPlainText }}<p id="post-body" class="e-content">{{.Content}}</p>{{ else }}<div id="post-body" class="e-content">{{.HTMLContent}}</div>{{ end }}</article>
|
<article class="{{.Font}} h-entry">{{if .Title}}<h2 id="title" class="p-name">{{.Title}}</h2>{{end}}{{ if .IsPlainText }}<p id="post-body" class="e-content">{{.Content}}</p>{{ else }}<div id="post-body" class="e-content">{{.HTMLContent}}</div>{{ end }}</article>
|
||||||
|
|
||||||
|
|
|
@ -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 }}
|
||||||
|
|
|
@ -11,12 +11,14 @@
|
||||||
<th>User</th>
|
<th>User</th>
|
||||||
<th>Joined</th>
|
<th>Joined</th>
|
||||||
<th>Type</th>
|
<th>Type</th>
|
||||||
|
<th>Status</th>
|
||||||
</tr>
|
</tr>
|
||||||
{{range .Users}}
|
{{range .Users}}
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="/admin/user/{{.Username}}">{{.Username}}</a></td>
|
<td><a href="/admin/user/{{.Username}}">{{.Username}}</a></td>
|
||||||
<td>{{.CreatedFriendly}}</td>
|
<td>{{.CreatedFriendly}}</td>
|
||||||
<td style="text-align:center">{{if .IsAdmin}}Admin{{else}}User{{end}}</td>
|
<td style="text-align:center">{{if .IsAdmin}}Admin{{else}}User{{end}}</td>
|
||||||
|
<td style="text-align:center">{{if .IsSilenced}}Silenced{{else}}Active{{end}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{end}}
|
{{end}}
|
||||||
</table>
|
</table>
|
||||||
|
|
|
@ -7,12 +7,43 @@ table.classy th {
|
||||||
h3 {
|
h3 {
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
|
td.active-suspend {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.active-suspend > input[type="submit"] {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: 5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 500px) {
|
||||||
|
td.active-suspend {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
td.active-suspend > input[type="submit"] {
|
||||||
|
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>
|
||||||
|
@ -38,6 +69,34 @@ h3 {
|
||||||
<th>Last Post</th>
|
<th>Last Post</th>
|
||||||
<td>{{if .LastPost}}{{.LastPost}}{{else}}Never{{end}}</td>
|
<td>{{if .LastPost}}{{.LastPost}}{{else}}Never{{end}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<form action="/admin/user/{{.User.Username}}/status" method="POST" {{if not .User.IsSilenced}}onsubmit="return confirmSilence()"{{end}}>
|
||||||
|
<a id="status"/>
|
||||||
|
<th>Status</th>
|
||||||
|
<td class="active-suspend">
|
||||||
|
{{if .User.IsSilenced}}
|
||||||
|
<p>Silenced</p>
|
||||||
|
<input type="submit" value="Unsilence"/>
|
||||||
|
{{else}}
|
||||||
|
<p>Active</p>
|
||||||
|
<input class="danger" type="submit" value="Silence" {{if .User.IsAdmin}}disabled{{end}}/>
|
||||||
|
{{end}}
|
||||||
|
</td>
|
||||||
|
</form>
|
||||||
|
</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>
|
||||||
|
@ -83,5 +142,19 @@ h3 {
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
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.");
|
||||||
|
}
|
||||||
|
|
||||||
|
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}}
|
||||||
|
|
|
@ -6,6 +6,9 @@
|
||||||
{{if .Flashes}}<ul class="errors">
|
{{if .Flashes}}<ul class="errors">
|
||||||
{{range .Flashes}}<li class="urgent">{{.}}</li>{{end}}
|
{{range .Flashes}}<li class="urgent">{{.}}</li>{{end}}
|
||||||
</ul>{{end}}
|
</ul>{{end}}
|
||||||
|
{{if .Suspended}}
|
||||||
|
{{template "user-suspended"}}
|
||||||
|
{{end}}
|
||||||
|
|
||||||
<h2 id="posts-header">drafts</h2>
|
<h2 id="posts-header">drafts</h2>
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,9 @@
|
||||||
<div class="content-container snug">
|
<div class="content-container snug">
|
||||||
<div id="overlay"></div>
|
<div id="overlay"></div>
|
||||||
|
|
||||||
|
{{if .Suspended}}
|
||||||
|
{{template "user-suspended"}}
|
||||||
|
{{end}}
|
||||||
<h2>Customize {{.DisplayTitle}} <a href="{{if .SingleUser}}/{{else}}/{{.Alias}}/{{end}}">view blog</a></h2>
|
<h2>Customize {{.DisplayTitle}} <a href="{{if .SingleUser}}/{{else}}/{{.Alias}}/{{end}}">view blog</a></h2>
|
||||||
|
|
||||||
{{if .Flashes}}<ul class="errors">
|
{{if .Flashes}}<ul class="errors">
|
||||||
|
|
|
@ -7,6 +7,9 @@
|
||||||
{{range .Flashes}}<li class="urgent">{{.}}</li>{{end}}
|
{{range .Flashes}}<li class="urgent">{{.}}</li>{{end}}
|
||||||
</ul>{{end}}
|
</ul>{{end}}
|
||||||
|
|
||||||
|
{{if .Suspended}}
|
||||||
|
{{template "user-suspended"}}
|
||||||
|
{{end}}
|
||||||
<h2>blogs</h2>
|
<h2>blogs</h2>
|
||||||
<ul class="atoms collections">
|
<ul class="atoms collections">
|
||||||
{{range $i, $el := .Collections}}<li class="collection"><h3>
|
{{range $i, $el := .Collections}}<li class="collection"><h3>
|
||||||
|
|
|
@ -22,13 +22,10 @@
|
||||||
</nav>
|
</nav>
|
||||||
</nav>
|
</nav>
|
||||||
{{else}}
|
{{else}}
|
||||||
{{ if .Chorus }}<nav id="full-nav">
|
<nav id="full-nav">
|
||||||
<div class="left-side">
|
<div class="left-side">
|
||||||
<h1><a href="/" title="Return to editor">{{.SiteName}}</a></h1>
|
<h1><a href="/" title="Return to editor">{{.SiteName}}</a></h1>
|
||||||
</div>
|
</div>
|
||||||
{{ else }}
|
|
||||||
<h1><a href="/" title="Return to editor">{{.SiteName}}</a></h1>
|
|
||||||
{{ end }}
|
|
||||||
<nav id="user-nav">
|
<nav id="user-nav">
|
||||||
{{if .Username}}
|
{{if .Username}}
|
||||||
<nav class="dropdown-nav">
|
<nav class="dropdown-nav">
|
||||||
|
@ -62,6 +59,7 @@
|
||||||
{{else}}
|
{{else}}
|
||||||
<a href="/me/c/"{{if eq .Path "/me/c/"}} class="selected"{{end}}>Blogs</a>
|
<a href="/me/c/"{{if eq .Path "/me/c/"}} class="selected"{{end}}>Blogs</a>
|
||||||
{{if not .DisableDrafts}}<a href="/me/posts/"{{if eq .Path "/me/posts/"}} class="selected"{{end}}>Drafts</a>{{end}}
|
{{if not .DisableDrafts}}<a href="/me/posts/"{{if eq .Path "/me/posts/"}} class="selected"{{end}}>Drafts</a>{{end}}
|
||||||
|
{{if and (and .LocalTimeline .CanViewReader) (not .Chorus)}}<a href="/read">Reader</a>{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
</nav>
|
</nav>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
5
templates/user/include/suspended.tmpl
Normal file
5
templates/user/include/suspended.tmpl
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{{define "user-suspended"}}
|
||||||
|
<div class="alert info">
|
||||||
|
<p><strong>Your account has been silenced.</strong> You can still access all of your posts and blogs, but no one else can currently see them.</p>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
|
@ -7,6 +7,9 @@ h3 { font-weight: normal; }
|
||||||
.section > *:not(input) { font-size: 0.86em; }
|
.section > *:not(input) { font-size: 0.86em; }
|
||||||
</style>
|
</style>
|
||||||
<div class="content-container snug regular">
|
<div class="content-container snug regular">
|
||||||
|
{{if .Suspended}}
|
||||||
|
{{template "user-suspended"}}
|
||||||
|
{{end}}
|
||||||
<h2>{{if .IsLogOut}}Before you go...{{else}}Account Settings {{if .IsAdmin}}<a href="/admin">admin settings</a>{{end}}{{end}}</h2>
|
<h2>{{if .IsLogOut}}Before you go...{{else}}Account Settings {{if .IsAdmin}}<a href="/admin">admin settings</a>{{end}}{{end}}</h2>
|
||||||
{{if .Flashes}}<ul class="errors">
|
{{if .Flashes}}<ul class="errors">
|
||||||
{{range .Flashes}}<li class="urgent">{{.}}</li>{{end}}
|
{{range .Flashes}}<li class="urgent">{{.}}</li>{{end}}
|
||||||
|
|
|
@ -17,6 +17,9 @@ td.none {
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class="content-container snug">
|
<div class="content-container snug">
|
||||||
|
{{if .Suspended}}
|
||||||
|
{{template "user-suspended"}}
|
||||||
|
{{end}}
|
||||||
<h2 id="posts-header">{{if .Collection}}{{.Collection.DisplayTitle}} {{end}}Stats</h2>
|
<h2 id="posts-header">{{if .Collection}}{{.Collection.DisplayTitle}} {{end}}Stats</h2>
|
||||||
|
|
||||||
<p>Stats for all time.</p>
|
<p>Stats for all time.</p>
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
12
users.go
12
users.go
|
@ -19,6 +19,13 @@ import (
|
||||||
"github.com/writeas/writefreely/key"
|
"github.com/writeas/writefreely/key"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type UserStatus int
|
||||||
|
|
||||||
|
const (
|
||||||
|
UserActive = iota
|
||||||
|
UserSilenced
|
||||||
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
userCredentials struct {
|
userCredentials struct {
|
||||||
Alias string `json:"alias" schema:"alias"`
|
Alias string `json:"alias" schema:"alias"`
|
||||||
|
@ -59,6 +66,7 @@ type (
|
||||||
HasPass bool `json:"has_pass"`
|
HasPass bool `json:"has_pass"`
|
||||||
Email zero.String `json:"email"`
|
Email zero.String `json:"email"`
|
||||||
Created time.Time `json:"created"`
|
Created time.Time `json:"created"`
|
||||||
|
Status UserStatus `json:"status"`
|
||||||
|
|
||||||
clearEmail string `json:"email"`
|
clearEmail string `json:"email"`
|
||||||
}
|
}
|
||||||
|
@ -118,3 +126,7 @@ func (u *User) IsAdmin() bool {
|
||||||
// TODO: get this from database
|
// TODO: get this from database
|
||||||
return u.ID == 1
|
return u.ID == 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *User) IsSilenced() bool {
|
||||||
|
return u.Status&UserSilenced != 0
|
||||||
|
}
|
||||||
|
|
|
@ -41,6 +41,14 @@ func (wfr wfResolver) FindUser(username string, host, requestHost string, r []we
|
||||||
log.Error("Unable to get blog: %v", err)
|
log.Error("Unable to get blog: %v", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
suspended, err := wfr.db.IsUserSuspended(c.OwnerID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("webfinger find user: check is suspended: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if suspended {
|
||||||
|
return nil, wfUserNotFoundErr
|
||||||
|
}
|
||||||
c.hostName = wfr.cfg.App.Host
|
c.hostName = wfr.cfg.App.Host
|
||||||
if wfr.cfg.App.SingleUser {
|
if wfr.cfg.App.SingleUser {
|
||||||
// Ensure handle matches user-chosen one on single-user blogs
|
// Ensure handle matches user-chosen one on single-user blogs
|
||||||
|
|
Loading…
Add table
Reference in a new issue