Browse Source

Merge pull request #174 from writeas/T661-disable-accounts

Add account suspension features
tags/v0.11.0^0
Matt Baer 4 years ago
committed by GitHub
parent
commit
bca678aee5
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 458 additions and 48 deletions
  1. +36
    -7
      account.go
  2. +40
    -0
      activitypub.go
  3. +25
    -0
      admin.go
  4. +36
    -4
      collections.go
  5. +43
    -9
      database.go
  6. +4
    -1
      errors.go
  7. +12
    -2
      feed.go
  8. +4
    -0
      invites.go
  9. +8
    -1
      less/core.less
  10. +2
    -0
      migrations/migrations.go
  11. +29
    -0
      migrations/v3.go
  12. +14
    -3
      pad.go
  13. +76
    -8
      posts.go
  14. +8
    -6
      read.go
  15. +1
    -0
      routes.go
  16. +8
    -4
      templates.go
  17. +3
    -0
      templates/chorus-collection-post.tmpl
  18. +3
    -0
      templates/chorus-collection.tmpl
  19. +3
    -0
      templates/collection-post.tmpl
  20. +3
    -0
      templates/collection-tags.tmpl
  21. +3
    -0
      templates/collection.tmpl
  22. +4
    -0
      templates/edit-meta.tmpl
  23. +5
    -1
      templates/pad.tmpl
  24. +3
    -0
      templates/password-collection.tmpl
  25. +4
    -1
      templates/post.tmpl
  26. +2
    -0
      templates/user/admin/users.tmpl
  27. +37
    -0
      templates/user/admin/view-user.tmpl
  28. +3
    -0
      templates/user/articles.tmpl
  29. +3
    -0
      templates/user/collection.tmpl
  30. +3
    -0
      templates/user/collections.tmpl
  31. +5
    -0
      templates/user/include/suspended.tmpl
  32. +3
    -0
      templates/user/settings.tmpl
  33. +3
    -0
      templates/user/stats.tmpl
  34. +12
    -0
      users.go
  35. +10
    -1
      webfinger.go

+ 36
- 7
account.go View File

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

suspended, err := app.db.IsUserSuspended(u.ID)
if err != nil {
log.Error("view articles: %v", err)
}
d := struct {
*UserPage
AnonymousPosts *[]PublicPost
Collections *[]Collection
Suspended bool
}{
UserPage: NewUserPage(app, r, u, u.Username+"'s Posts", f),
AnonymousPosts: p,
Collections: c,
Suspended: suspended,
}
d.UserPage.SetMessaging(u)
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)
// 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 {
*UserPage
Collections *[]Collection
@@ -786,11 +797,13 @@ func viewCollections(app *App, u *User, w http.ResponseWriter, r *http.Request)
UsedCollections, TotalCollections int

NewBlogsDisabled bool
Suspended bool
}{
UserPage: NewUserPage(app, r, u, u.Username+"'s Blogs", f),
Collections: c,
UsedCollections: int(uc),
NewBlogsDisabled: !app.cfg.App.CanCreateBlogs(uc),
Suspended: suspended,
}
d.UserPage.SetMessaging(u)
showUserPage(w, "collections", d)
@@ -808,13 +821,20 @@ func viewEditCollection(app *App, u *User, w http.ResponseWriter, r *http.Reques
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)
obj := struct {
*UserPage
*Collection
Suspended bool
}{
UserPage: NewUserPage(app, r, u, "Edit "+c.DisplayTitle(), flashes),
Collection: c,
Suspended: suspended,
}

showUserPage(w, "collection", obj)
@@ -976,17 +996,24 @@ func viewStats(app *App, u *User, w http.ResponseWriter, r *http.Request) error
titleStats = c.DisplayTitle() + " "
}

suspended, err := app.db.IsUserSuspended(u.ID)
if err != nil {
log.Error("view stats: %v", err)
return err
}
obj := struct {
*UserPage
VisitsBlog string
Collection *Collection
TopPosts *[]PublicPost
APFollowers int
Suspended bool
}{
UserPage: NewUserPage(app, r, u, titleStats+"Stats", flashes),
VisitsBlog: alias,
Collection: c,
TopPosts: topPosts,
Suspended: suspended,
}
if app.cfg.App.Federation {
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 {
*UserPage
Email string
HasPass bool
IsLogOut bool
Email string
HasPass bool
IsLogOut bool
Suspended bool
}{
UserPage: NewUserPage(app, r, u, "Account Settings", flashes),
Email: fullUser.EmailClear(app.keys),
HasPass: passIsSet,
IsLogOut: r.FormValue("logout") == "1",
UserPage: NewUserPage(app, r, u, "Account Settings", flashes),
Email: fullUser.EmailClear(app.keys),
HasPass: passIsSet,
IsLogOut: r.FormValue("logout") == "1",
Suspended: fullUser.IsSilenced(),
}

showUserPage(w, "settings", obj)


+ 40
- 0
activitypub.go View File

@@ -80,6 +80,14 @@ func handleFetchCollectionActivities(app *App, w http.ResponseWriter, r *http.Re
if err != nil {
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

p := c.PersonObject()
@@ -105,6 +113,14 @@ func handleFetchCollectionOutbox(app *App, w http.ResponseWriter, r *http.Reques
if err != nil {
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

if app.cfg.App.SingleUser {
@@ -158,6 +174,14 @@ func handleFetchCollectionFollowers(app *App, w http.ResponseWriter, r *http.Req
if err != nil {
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

accountRoot := c.FederatedAccount()
@@ -204,6 +228,14 @@ func handleFetchCollectionFollowing(app *App, w http.ResponseWriter, r *http.Req
if err != nil {
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

accountRoot := c.FederatedAccount()
@@ -238,6 +270,14 @@ func handleFetchCollectionInbox(app *App, w http.ResponseWriter, r *http.Request
// TODO: return Reject?
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

if debugging {


+ 25
- 0
admin.go View File

@@ -241,12 +241,37 @@ func handleViewAdminUser(app *App, u *User, w http.ResponseWriter, r *http.Reque
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))


+ 36
- 4
collections.go View File

@@ -71,6 +71,7 @@ type (
CurrentPage int
TotalPages int
Format *CollectionFormat
Suspended bool
}
SubmittedCollection struct {
// Data used for updating a given collection
@@ -379,6 +380,7 @@ func newCollection(app *App, w http.ResponseWriter, r *http.Request) error {
}

var userID int64
var err error
if reqJSON && !c.Web {
accessToken = r.Header.Get("Authorization")
if accessToken == "" {
@@ -395,6 +397,14 @@ func newCollection(app *App, w http.ResponseWriter, r *http.Request) error {
}
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) {
return impart.HTTPError{http.StatusPreconditionFailed, "Collection alias isn't valid."}
@@ -477,6 +487,7 @@ func fetchCollection(app *App, w http.ResponseWriter, r *http.Request) error {
res.Owner = u
}
}
// TODO: check suspended
app.db.GetPostsCount(res, isCollOwner)
// Strip non-public information
res.Collection.ForPublic()
@@ -725,9 +736,14 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro
if c == nil || err != nil {
return err
}

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
if strings.Contains(r.Header.Get("Accept"), "application/activity+json") {
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)
}
}
if !isOwner && suspended {
return ErrCollectionNotFound
}
displayPage.Suspended = isOwner && suspended
displayPage.Owner = owner
coll.Owner = displayPage.Owner

@@ -886,6 +906,10 @@ func handleViewCollectionTag(app *App, w http.ResponseWriter, r *http.Request) e
log.Error("Error getting user for collection: %v", err)
}
}
if !isOwner && u.IsSilenced() {
return ErrCollectionNotFound
}
displayPage.Suspended = u.IsSilenced()
displayPage.Owner = owner
coll.Owner = displayPage.Owner
// Add more data
@@ -924,11 +948,10 @@ func existingCollection(app *App, w http.ResponseWriter, r *http.Request) error
collAlias := vars["alias"]
isWeb := r.FormValue("web") == "1"

var u *User
u := &User{}
if reqJSON && !isWeb {
// Ensure an access token was given
accessToken := r.Header.Get("Authorization")
u = &User{}
u.ID = app.db.GetUserID(accessToken)
if u.ID == -1 {
return ErrBadAccessToken
@@ -940,6 +963,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" {
err := app.db.DeleteCollection(collAlias, u.ID)
if err != nil {
@@ -952,7 +985,6 @@ func existingCollection(app *App, w http.ResponseWriter, r *http.Request) error
}

c := SubmittedCollection{OwnerID: uint64(u.ID)}
var err error

if reqJSON {
// Decode JSON request


+ 43
- 9
database.go View File

@@ -296,7 +296,7 @@ func (db *datastore) CreateCollection(cfg *config.Config, alias, title string, u
func (db *datastore) GetUserByID(id int64) (*User, error) {
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 {
case err == sql.ErrNoRows:
return nil, ErrUserNotFound
@@ -308,6 +308,23 @@ func (db *datastore) GetUserByID(id int64) (*User, error) {
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
// authenticating with the account, such a passphrase or email address.
// Any errors are reported to admin and silently quashed, returning false as the
@@ -347,7 +364,7 @@ func (db *datastore) IsUserPassSet(id int64) (bool, error) {
func (db *datastore) GetUserForAuth(username string) (*User, error) {
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 {
case err == sql.ErrNoRows:
// Check if they've entered the wrong, unnormalized username
@@ -370,7 +387,7 @@ func (db *datastore) GetUserForAuth(username string) (*User, error) {
func (db *datastore) GetUserForAuthByID(userID int64) (*User, error) {
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 {
case err == sql.ErrNoRows:
return nil, ErrUserNotFound
@@ -1629,7 +1646,11 @@ func (db *datastore) GetMeStats(u *User) userMeStats {
}

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 {
log.Error("Unable to fetch collections count: %v", err)
}
@@ -1637,7 +1658,11 @@ func (db *datastore) GetTotalCollections() (collCount 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 {
log.Error("Unable to fetch posts count: %v", err)
}
@@ -2359,17 +2384,17 @@ func (db *datastore) GetAllUsers(page uint) (*[]User, error) {
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 {
log.Error("Failed selecting from posts: %v", err)
return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve user posts."}
log.Error("Failed selecting from users: %v", err)
return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve all users."}
}
defer rows.Close()

users := []User{}
for rows.Next() {
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 {
log.Error("Failed scanning GetAllUsers() row: %v", err)
break
@@ -2406,6 +2431,15 @@ func (db *datastore) GetUserLastPostTime(id int64) (*time.Time, error) {
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) {
var t time.Time
err := db.QueryRow("SELECT created FROM posts WHERE collection_id = ? ORDER BY created DESC LIMIT 1", id).Scan(&t)


+ 4
- 1
errors.go View File

@@ -11,8 +11,9 @@
package writefreely

import (
"github.com/writeas/impart"
"net/http"

"github.com/writeas/impart"
)

// Commonly returned HTTP errors
@@ -46,6 +47,8 @@ var (

ErrUserNotFound = impart.HTTPError{http.StatusNotFound, "User doesn't exist."}
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


+ 12
- 2
feed.go View File

@@ -12,12 +12,13 @@ package writefreely

import (
"fmt"
"net/http"
"time"

. "github.com/gorilla/feeds"
"github.com/gorilla/mux"
stripmd "github.com/writeas/go-strip-markdown"
"github.com/writeas/web-core/log"
"net/http"
"time"
)

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

if c.IsPrivate() || c.IsProtected() {


+ 4
- 0
invites.go View File

@@ -78,6 +78,10 @@ func handleCreateUserInvite(app *App, u *User, w http.ResponseWriter, r *http.Re
muVal := r.FormValue("uses")
expVal := r.FormValue("expires")

if u.IsSilenced() {
return ErrUserSuspended
}

var err error
var maxUses int
if muVal != "0" {


+ 8
- 1
less/core.less View File

@@ -516,10 +516,17 @@ abbr {
body#collection article p, body#subpage 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;
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 {
&.norm, &.sans, &.wrap {
line-height: 1.4em;


+ 2
- 0
migrations/migrations.go View File

@@ -13,6 +13,7 @@ package migrations

import (
"database/sql"

"github.com/writeas/web-core/log"
)

@@ -57,6 +58,7 @@ func (m *migration) Migrate(db *datastore) error {
var migrations = []Migration{
New("support user invites", supportUserInvites), // -> V1 (v0.8.0)
New("support dynamic instance pages", supportInstancePages), // V1 -> V2 (v0.9.0)
New("support users suspension", supportUserStatus), // V2 -> V3 (v0.11.0)
}

// CurrentVer returns the current migration version the application is on


+ 29
- 0
migrations/v3.go View 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 supportUserStatus(db *datastore) error {
t, err := db.Begin()

_, err = t.Exec(`ALTER TABLE users ADD COLUMN status ` + db.typeInt() + ` DEFAULT '0' NOT NULL`)
if err != nil {
t.Rollback()
return err
}

err = t.Commit()
if err != nil {
t.Rollback()
return err
}

return nil
}

+ 14
- 3
pad.go View File

@@ -35,9 +35,10 @@ func handleViewPad(app *App, w http.ResponseWriter, r *http.Request) error {
}
appData := &struct {
page.StaticPage
Post *RawPost
User *User
Blogs *[]Collection
Post *RawPost
User *User
Blogs *[]Collection
Suspended bool

Editing bool // True if we're modifying an existing post
EditCollection *Collection // Collection of the post we're editing, if any
@@ -52,6 +53,10 @@ func handleViewPad(app *App, w http.ResponseWriter, r *http.Request) error {
if err != nil {
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
@@ -119,12 +124,18 @@ func handleViewMeta(app *App, w http.ResponseWriter, r *http.Request) error {
EditCollection *Collection // Collection of the post we're editing, if any
Flashes []string
NeedsToken bool
Suspended bool
}{
StaticPage: pageForReq(app, r),
Post: &RawPost{Font: "norm"},
User: getUserSession(app, r),
}
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 == "" {
return ErrPostNotFound


+ 76
- 8
posts.go View File

@@ -381,6 +381,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
if content == "" {
gone = true
@@ -428,9 +434,10 @@ func handleViewPost(app *App, w http.ResponseWriter, r *http.Request) error {
page := struct {
*AnonymousPost
page.StaticPage
Username string
IsOwner bool
SiteURL string
Username string
IsOwner bool
SiteURL string
Suspended bool
}{
AnonymousPost: post,
StaticPage: pageForReq(app, r),
@@ -441,6 +448,10 @@ func handleViewPost(app *App, w http.ResponseWriter, r *http.Request) error {
page.IsOwner = ownerID.Valid && ownerID.Int64 == u.ID
}

if !page.IsOwner && suspended {
return ErrPostNotFound
}
page.Suspended = suspended
err = templates["post"].ExecuteTemplate(w, "post", page)
if err != nil {
log.Error("Post template execute error: %v", err)
@@ -497,6 +508,15 @@ func newPost(app *App, w http.ResponseWriter, r *http.Request) error {
} else {
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 {
return ErrNotLoggedIn
}
@@ -509,7 +529,7 @@ func newPost(app *App, w http.ResponseWriter, r *http.Request) error {
var p *SubmittedPost
if reqJSON {
decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&p)
err = decoder.Decode(&p)
if err != nil {
log.Error("Couldn't parse new post JSON request: %v\n", err)
return ErrBadJSON
@@ -555,7 +575,6 @@ func newPost(app *App, w http.ResponseWriter, r *http.Request) error {

var newPost *PublicPost = &PublicPost{}
var coll *Collection
var err error
if accessToken != "" {
newPost, err = app.db.CreateOwnedPost(p, accessToken, collAlias, app.cfg.App.Host)
} else {
@@ -663,6 +682,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
p.ID = postID

@@ -857,11 +885,20 @@ func addPost(app *App, w http.ResponseWriter, r *http.Request) error {
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:
// [{"id": "...", "token": "..."}]
var claims *[]ClaimPostRequest
decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&claims)
err = decoder.Decode(&claims)
if err != nil {
return ErrBadJSONArray
}
@@ -951,13 +988,22 @@ func pinPost(app *App, w http.ResponseWriter, r *http.Request) error {
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
var posts []struct {
ID string `json:"id"`
Position int64 `json:"position"`
}
decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&posts)
err = decoder.Decode(&posts)
if err != nil {
return ErrBadJSONArray
}
@@ -993,6 +1039,7 @@ func pinPost(app *App, w http.ResponseWriter, r *http.Request) error {

func fetchPost(app *App, w http.ResponseWriter, r *http.Request) error {
var collID int64
var ownerID int64
var coll *Collection
var err error
vars := mux.Vars(r)
@@ -1008,12 +1055,22 @@ func fetchPost(app *App, w http.ResponseWriter, r *http.Request) error {
return err
}
collID = coll.ID
ownerID = coll.OwnerID
}

p, err := app.db.GetPost(vars["post"], collID)
if err != nil {
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()

@@ -1275,6 +1332,12 @@ func viewCollectionPost(app *App, w http.ResponseWriter, r *http.Request) error
}
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
if c.IsPrivate() && (u == nil || u.ID != c.OwnerID) {
return ErrPostNotFound
@@ -1327,10 +1390,13 @@ Are you sure it was ever here?`,
return err
}
}
p.IsOwner = owner != nil && p.OwnerID.Valid && owner.ID == p.OwnerID.Int64
p.IsOwner = owner != nil && p.OwnerID.Valid && u.ID == p.OwnerID.Int64
p.Collection = coll
p.IsTopLevel = app.cfg.App.SingleUser

if !p.IsOwner && suspended {
return ErrPostNotFound
}
// Check if post has been unpublished
if p.Content == "" && p.Title.String == "" {
return impart.HTTPError{http.StatusGone, "Post was unpublished."}
@@ -1380,12 +1446,14 @@ Are you sure it was ever here?`,
IsFound bool
IsAdmin bool
CanInvite bool
Suspended bool
}{
PublicPost: p,
StaticPage: pageForReq(app, r),
IsOwner: cr.isCollOwner,
IsCustomDomain: cr.isCustomDomain,
IsFound: postFound,
Suspended: suspended,
}
tp.IsAdmin = u != nil && u.IsAdmin()
tp.CanInvite = canUserInvite(app.cfg, tp.IsAdmin)


+ 8
- 6
read.go View File

@@ -13,6 +13,12 @@ package writefreely
import (
"database/sql"
"fmt"
"html/template"
"math"
"net/http"
"strconv"
"time"

. "github.com/gorilla/feeds"
"github.com/gorilla/mux"
stripmd "github.com/writeas/go-strip-markdown"
@@ -20,11 +26,6 @@ import (
"github.com/writeas/web-core/log"
"github.com/writeas/web-core/memo"
"github.com/writeas/writefreely/page"
"html/template"
"math"
"net/http"
"strconv"
"time"
)

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
FROM collections c
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`)
if err != nil {
log.Error("Failed selecting from posts: %v", err)


+ 1
- 0
routes.go View File

@@ -144,6 +144,7 @@ func InitRoutes(apper Apper, r *mux.Router) *mux.Router {
write.HandleFunc("/admin", handler.Admin(handleViewAdminDash)).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}/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/page/{slug}", handler.Admin(handleViewAdminPage)).Methods("GET")


+ 8
- 4
templates.go View File

@@ -11,10 +11,6 @@
package writefreely

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"
"io"
"io/ioutil"
@@ -22,6 +18,11 @@ import (
"os"
"path/filepath"
"strings"

"github.com/dustin/go-humanize"
"github.com/writeas/web-core/l10n"
"github.com/writeas/web-core/log"
"github.com/writeas/writefreely/config"
)

var (
@@ -63,6 +64,7 @@ func initTemplate(parentDir, name string) {
filepath.Join(parentDir, templatesDir, name+".tmpl"),
filepath.Join(parentDir, templatesDir, "include", "footer.tmpl"),
filepath.Join(parentDir, templatesDir, "base.tmpl"),
filepath.Join(parentDir, templatesDir, "user", "include", "suspended.tmpl"),
}
if name == "collection" || name == "collection-tags" || name == "chorus-collection" {
// 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,
filepath.Join(parentDir, templatesDir, "include", "footer.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,
filepath.Join(parentDir, templatesDir, "user", "include", "header.tmpl"),
filepath.Join(parentDir, templatesDir, "user", "include", "footer.tmpl"),
filepath.Join(parentDir, templatesDir, "user", "include", "suspended.tmpl"),
))
}



+ 3
- 0
templates/chorus-collection-post.tmpl View File

@@ -65,6 +65,9 @@ article time.dt-published {

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

{{ if .Collection.ShowFooterBranding }}


+ 3
- 0
templates/chorus-collection.tmpl View File

@@ -61,6 +61,9 @@ body#collection header nav.tabs a:first-child {
<body id="collection" itemscope itemtype="http://schema.org/WebPage">
{{template "user-navigation" .}}
{{if .Suspended}}
{{template "user-suspended"}}
{{end}}
<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>
{{if .Description}}<p class="description p-note">{{.Description}}</p>{{end}}


+ 3
- 0
templates/collection-post.tmpl View File

@@ -59,6 +59,9 @@
</nav>
</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>

{{ if .Collection.ShowFooterBranding }}


+ 3
- 0
templates/collection-tags.tmpl View File

@@ -53,6 +53,9 @@
</nav>
</header>
{{if .Suspended}}
{{template "user-suspended"}}
{{end}}
{{if .Posts}}<section id="wrapper" itemscope itemtype="http://schema.org/Blog">{{else}}<div id="wrapper">{{end}}
<h1>{{.Tag}}</h1>
{{template "posts" .}}


+ 3
- 0
templates/collection.tmpl View File

@@ -62,6 +62,9 @@
</ul></nav>{{end}}
<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>
{{if .Description}}<p class="description p-note">{{.Description}}</p>{{end}}
{{/*if not .Public/*}}


+ 4
- 0
templates/edit-meta.tmpl View File

@@ -269,6 +269,10 @@
<script src="/js/h.js"></script>
<script>
function updateMeta() {
if ({{.Suspended}}) {
alert('Your account is currently supsended, editing posts is disabled.');
return
}
document.getElementById('create-error').style.display = 'none';
var $created = document.getElementById('created');
var dateStr = $created.value.trim();


+ 5
- 1
templates/pad.tmpl View File

@@ -131,8 +131,12 @@
{{else}}var canPublish = true;{{end}}
var publishing = false;
var justPublished = false;
var suspended = {{.Suspended}};
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 (!token) {
alert("You don't have permission to update this post.");


+ 3
- 0
templates/password-collection.tmpl View File

@@ -25,6 +25,9 @@

</head>
<body id="collection" itemscope itemtype="http://schema.org/WebPage">
{{if .Suspended}}
{{template "user-supsended"}}
{{end}}
<header>
<h1 dir="{{.Direction}}" id="blog-title"><a href="/{{.Alias}}/" class="h-card p-author u-url" rel="me author">{{.DisplayTitle}}</a></h1>
</header>


+ 4
- 1
templates/post.tmpl View File

@@ -35,7 +35,6 @@
{{template "highlighting" .}}
</head>
<body id="post">

<header>
<h1 dir="{{.Direction}}"><a href="/">{{.SiteName}}</a></h1>
<nav>
@@ -49,6 +48,10 @@
{{ end }}
</nav>
</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>



+ 2
- 0
templates/user/admin/users.tmpl View File

@@ -11,12 +11,14 @@
<th>User</th>
<th>Joined</th>
<th>Type</th>
<th>Status</th>
</tr>
{{range .Users}}
<tr>
<td><a href="/admin/user/{{.Username}}">{{.Username}}</a></td>
<td>{{.CreatedFriendly}}</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>
{{end}}
</table>


+ 37
- 0
templates/user/admin/view-user.tmpl View File

@@ -7,6 +7,24 @@ table.classy th {
h3 {
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;
@@ -52,6 +70,21 @@ input.copy-text {
<td>{{if .LastPost}}{{.LastPost}}{{else}}Never{{end}}</td>
</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}}
@@ -110,6 +143,10 @@ input.copy-text {
</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();


+ 3
- 0
templates/user/articles.tmpl View File

@@ -6,6 +6,9 @@
{{if .Flashes}}<ul class="errors">
{{range .Flashes}}<li class="urgent">{{.}}</li>{{end}}
</ul>{{end}}
{{if .Suspended}}
{{template "user-suspended"}}
{{end}}

<h2 id="posts-header">drafts</h2>



+ 3
- 0
templates/user/collection.tmpl View File

@@ -8,6 +8,9 @@
<div class="content-container snug">
<div id="overlay"></div>

{{if .Suspended}}
{{template "user-suspended"}}
{{end}}
<h2>Customize {{.DisplayTitle}} <a href="{{if .SingleUser}}/{{else}}/{{.Alias}}/{{end}}">view blog</a></h2>

{{if .Flashes}}<ul class="errors">


+ 3
- 0
templates/user/collections.tmpl View File

@@ -7,6 +7,9 @@
{{range .Flashes}}<li class="urgent">{{.}}</li>{{end}}
</ul>{{end}}

{{if .Suspended}}
{{template "user-suspended"}}
{{end}}
<h2>blogs</h2>
<ul class="atoms collections">
{{range $i, $el := .Collections}}<li class="collection"><h3>


+ 5
- 0
templates/user/include/suspended.tmpl View 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}}

+ 3
- 0
templates/user/settings.tmpl View File

@@ -7,6 +7,9 @@ h3 { font-weight: normal; }
.section > *:not(input) { font-size: 0.86em; }
</style>
<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>
{{if .Flashes}}<ul class="errors">
{{range .Flashes}}<li class="urgent">{{.}}</li>{{end}}


+ 3
- 0
templates/user/stats.tmpl View File

@@ -17,6 +17,9 @@ td.none {
</style>

<div class="content-container snug">
{{if .Suspended}}
{{template "user-suspended"}}
{{end}}
<h2 id="posts-header">{{if .Collection}}{{.Collection.DisplayTitle}} {{end}}Stats</h2>

<p>Stats for all time.</p>


+ 12
- 0
users.go View File

@@ -19,6 +19,13 @@ import (
"github.com/writeas/writefreely/key"
)

type UserStatus int

const (
UserActive = iota
UserSilenced
)

type (
userCredentials struct {
Alias string `json:"alias" schema:"alias"`
@@ -59,6 +66,7 @@ type (
HasPass bool `json:"has_pass"`
Email zero.String `json:"email"`
Created time.Time `json:"created"`
Status UserStatus `json:"status"`

clearEmail string `json:"email"`
}
@@ -118,3 +126,7 @@ func (u *User) IsAdmin() bool {
// TODO: get this from database
return u.ID == 1
}

func (u *User) IsSilenced() bool {
return u.Status&UserSilenced != 0
}

+ 10
- 1
webfinger.go View File

@@ -11,11 +11,12 @@
package writefreely

import (
"net/http"

"github.com/writeas/go-webfinger"
"github.com/writeas/impart"
"github.com/writeas/web-core/log"
"github.com/writeas/writefreely/config"
"net/http"
)

type wfResolver struct {
@@ -37,6 +38,14 @@ func (wfr wfResolver) FindUser(username string, host, requestHost string, r []we
log.Error("Unable to get blog: %v", 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
if wfr.cfg.App.SingleUser {
// Ensure handle matches user-chosen one on single-user blogs


Loading…
Cancel
Save