Browse Source

Add account suspension features

This renders all requests for that user's posts, collections and related
ActivityPub endpoints with 404 responses.

While suspended, users may not create or edit posts or collections.

User status is listed in the admin user page

Admin view of user details shows status and now has a button to activate
or suspend a user.
T661-disable-accounts
Rob Loranger 4 years ago
parent
commit
77f7b4a522
No known key found for this signature in database GPG Key ID: D6F1633A4F0903B8
23 changed files with 381 additions and 56 deletions
  1. +16
    -13
      account.go
  2. +40
    -0
      activitypub.go
  3. +30
    -4
      admin.go
  4. +32
    -2
      collections.go
  5. +42
    -9
      database.go
  6. +4
    -1
      errors.go
  7. +12
    -2
      feed.go
  8. +9
    -4
      invites.go
  9. +2
    -0
      migrations/migrations.go
  10. +29
    -0
      migrations/v3.go
  11. +17
    -5
      pad.go
  12. +69
    -4
      posts.go
  13. +8
    -6
      read.go
  14. +5
    -3
      routes.go
  15. +1
    -0
      schema.sql
  16. +2
    -1
      sqlite.sql
  17. +4
    -0
      templates/edit-meta.tmpl
  18. +5
    -1
      templates/pad.tmpl
  19. +5
    -0
      templates/user/admin/users.tmpl
  20. +32
    -0
      templates/user/admin/view-user.tmpl
  21. +6
    -0
      templates/user/settings.tmpl
  22. +1
    -0
      users.go
  23. +10
    -1
      webfinger.go

+ 16
- 13
account.go View File

@@ -13,6 +13,13 @@ package writefreely
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"html/template"
"net/http"
"regexp"
"strings"
"sync"
"time"

"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/gorilla/sessions" "github.com/gorilla/sessions"
"github.com/guregu/null/zero" "github.com/guregu/null/zero"
@@ -22,12 +29,6 @@ import (
"github.com/writeas/web-core/log" "github.com/writeas/web-core/log"
"github.com/writeas/writefreely/author" "github.com/writeas/writefreely/author"
"github.com/writeas/writefreely/page" "github.com/writeas/writefreely/page"
"html/template"
"net/http"
"regexp"
"strings"
"sync"
"time"
) )


type ( type (
@@ -1011,14 +1012,16 @@ func viewSettings(app *App, u *User, w http.ResponseWriter, r *http.Request) err


obj := struct { obj := struct {
*UserPage *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.Suspended,
} }


showUserPage(w, "settings", obj) 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 { if err != nil {
return err return err
} }
suspended, err := app.db.IsUserSuspended(c.OwnerID)
if err != nil {
log.Error("fetch collection inbox: get owner: %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()
@@ -105,6 +113,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 inbox: get owner: %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 {
@@ -158,6 +174,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 inbox: get owner: %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()
@@ -204,6 +228,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 inbox: get owner: %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()
@@ -238,6 +270,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: get owner: %v", err)
return ErrInternalGeneral
}
if suspended {
return ErrCollectionNotFound
}
c.hostName = app.cfg.App.Host c.hostName = app.cfg.App.Host


if debugging { if debugging {


+ 30
- 4
admin.go View File

@@ -13,16 +13,17 @@ package writefreely
import ( import (
"database/sql" "database/sql"
"fmt" "fmt"
"net/http"
"runtime"
"strconv"
"time"

"github.com/gogits/gogs/pkg/tool" "github.com/gogits/gogs/pkg/tool"
"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/writefreely/config" "github.com/writeas/writefreely/config"
"net/http"
"runtime"
"strconv"
"time"
) )


var ( var (
@@ -229,6 +230,31 @@ func handleViewAdminUser(app *App, u *User, w http.ResponseWriter, r *http.Reque
return nil return nil
} }


func handleAdminToggleUserSuspended(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"}
}

userToToggle, 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 userToToggle.Suspended {
err = app.db.SetUserSuspended(userToToggle.ID, false)
} else {
err = app.db.SetUserSuspended(userToToggle.ID, true)
}
if err != nil {
log.Error("toggle user suspended: %v", err)
return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not toggle user suspended: %v")}
}
// TODO: invalidate sessions
return impart.HTTPError{http.StatusFound, fmt.Sprintf("/admin/user/%s#status", 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


+ 32
- 2
collections.go View File

@@ -379,6 +379,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 +396,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: get user: %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."}
@@ -724,6 +733,15 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro
return err return err
} }


suspended, err := app.db.IsUserSuspended(c.OwnerID)
if err != nil {
log.Error("view collection: get owner: %v", err)
return ErrInternalGeneral
}

if suspended {
return ErrCollectionNotFound
}
// 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()
@@ -824,6 +842,10 @@ func handleViewCollectionTag(app *App, w http.ResponseWriter, r *http.Request) e
return err return err
} }


if u.Suspended {
return ErrCollectionNotFound
}

page := getCollectionPage(vars) page := getCollectionPage(vars)


c, err := processCollectionPermissions(app, cr, u, w, r) c, err := processCollectionPermissions(app, cr, u, w, r)
@@ -916,7 +938,6 @@ func existingCollection(app *App, w http.ResponseWriter, r *http.Request) error
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
@@ -928,6 +949,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: get user suspended status: %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 {
@@ -940,7 +971,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


+ 42
- 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) { 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, suspended FROM users WHERE id = ?", id).Scan(&u.Username, &u.HashedPass, &u.Email, &u.Created, &u.Suspended)
switch { switch {
case err == sql.ErrNoRows: case err == sql.ErrNoRows:
return nil, ErrUserNotFound return nil, ErrUserNotFound
@@ -308,6 +308,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 suspended FROM users WHERE id = ?", id).Scan(&u.Suspended)
switch {
case err == sql.ErrNoRows:
return false, ErrUserNotFound
case err != nil:
log.Error("Couldn't SELECT user password: %v", err)
return false, err
}

return u.Suspended, 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
@@ -347,7 +364,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, suspended FROM users WHERE username = ?", username).Scan(&u.ID, &u.HashedPass, &u.Email, &u.Created, &u.Suspended)
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
@@ -370,7 +387,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, suspended FROM users WHERE id = ?", u.ID).Scan(&u.ID, &u.HashedPass, &u.Email, &u.Created, &u.Suspended)
switch { switch {
case err == sql.ErrNoRows: case err == sql.ErrNoRows:
return nil, ErrUserNotFound return nil, ErrUserNotFound
@@ -1624,7 +1641,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.suspended = 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)
} }
@@ -1632,7 +1653,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.Suspended = 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)
} }
@@ -2341,17 +2366,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, suspended FROM users ORDER BY created DESC LIMIT " + limitStr)
if err != nil { 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() 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.Suspended)
if err != nil { if err != nil {
log.Error("Failed scanning GetAllUsers() row: %v", err) log.Error("Failed scanning GetAllUsers() row: %v", err)
break break
@@ -2388,6 +2413,14 @@ func (db *datastore) GetUserLastPostTime(id int64) (*time.Time, error) {
return &t, nil return &t, nil
} }


func (db *datastore) SetUserSuspended(id int64, suspend bool) error {
_, err := db.Exec("UPDATE users SET suspended = ? WHERE id = ?", suspend, id)
if err != nil {
return fmt.Errorf("failed to update user suspended 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)


+ 4
- 1
errors.go View File

@@ -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 suspended, contact the administrator."}
) )


// Post operation errors // Post operation errors


+ 12
- 2
feed.go View File

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


+ 9
- 4
invites.go View File

@@ -12,15 +12,16 @@ package writefreely


import ( import (
"database/sql" "database/sql"
"html/template"
"net/http"
"strconv"
"time"

"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/writeas/impart" "github.com/writeas/impart"
"github.com/writeas/nerds/store" "github.com/writeas/nerds/store"
"github.com/writeas/web-core/log" "github.com/writeas/web-core/log"
"github.com/writeas/writefreely/page" "github.com/writeas/writefreely/page"
"html/template"
"net/http"
"strconv"
"time"
) )


type Invite struct { type Invite struct {
@@ -77,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.Suspended {
return ErrUserSuspended
}

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


+ 2
- 0
migrations/migrations.go View File

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


import ( import (
"database/sql" "database/sql"

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


@@ -57,6 +58,7 @@ 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 users suspension", supportUserSuspension), // V2 -> V3 ()
} }


// CurrentVer returns the current migration version the application is on // 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 supportUserSuspension(db *datastore) error {
t, err := db.Begin()

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

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

return nil
}

+ 17
- 5
pad.go View File

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


import ( import (
"net/http"
"strings"

"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/writeas/impart" "github.com/writeas/impart"
"github.com/writeas/web-core/log" "github.com/writeas/web-core/log"
"github.com/writeas/writefreely/page" "github.com/writeas/writefreely/page"
"net/http"
"strings"
) )


func handleViewPad(app *App, w http.ResponseWriter, r *http.Request) error { func handleViewPad(app *App, w http.ResponseWriter, r *http.Request) error {
@@ -34,9 +35,10 @@ func handleViewPad(app *App, w http.ResponseWriter, r *http.Request) error {
} }
appData := &struct { appData := &struct {
page.StaticPage 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 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
@@ -51,6 +53,10 @@ 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
@@ -121,12 +127,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


+ 69
- 4
posts.go View File

@@ -380,6 +380,16 @@ 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: get collection owner: %v", err)
return ErrInternalGeneral
}

if suspended {
return ErrPostNotFound
}

// Check if post has been unpublished // Check if post has been unpublished
if content == "" { if content == "" {
gone = true gone = true
@@ -496,6 +506,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: get user: %v", err)
return ErrInternalGeneral
}
if suspended {
return ErrUserSuspended
}

if userID == -1 { if userID == -1 {
return ErrNotLoggedIn return ErrNotLoggedIn
} }
@@ -508,7 +527,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 +573,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 {
@@ -662,6 +680,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: get user: %v", err)
return ErrInternalGeneral
}
if suspended {
return ErrUserSuspended
}

// Modify post struct // Modify post struct
p.ID = postID p.ID = postID


@@ -856,11 +883,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: get user: %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 +986,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: get user: %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 +1037,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 +1053,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: get owner: %v", err)
return ErrInternalGeneral
}

if suspended {
return ErrPostNotFound
}


p.extractData() p.extractData()


@@ -1270,6 +1326,15 @@ 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: get owner: %v", err)
return ErrInternalGeneral
}

if suspended {
return ErrPostNotFound
}
// 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


+ 8
- 6
read.go View File

@@ -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 (
@@ -62,7 +63,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.suspended = 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)


+ 5
- 3
routes.go View File

@@ -11,13 +11,14 @@
package writefreely package writefreely


import ( import (
"net/http"
"path/filepath"
"strings"

"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/writeas/go-webfinger" "github.com/writeas/go-webfinger"
"github.com/writeas/web-core/log" "github.com/writeas/web-core/log"
"github.com/writefreely/go-nodeinfo" "github.com/writefreely/go-nodeinfo"
"net/http"
"path/filepath"
"strings"
) )


// InitStaticRoutes adds routes for serving static files. // InitStaticRoutes adds routes for serving static files.
@@ -143,6 +144,7 @@ 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}", handler.Admin(handleAdminToggleUserSuspended)).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")


+ 1
- 0
schema.sql View File

@@ -225,6 +225,7 @@ CREATE TABLE IF NOT EXISTS `users` (
`password` char(60) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, `password` char(60) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL,
`email` varbinary(255) DEFAULT NULL, `email` varbinary(255) DEFAULT NULL,
`created` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `created` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`suspended` tinyint(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
UNIQUE KEY `username` (`username`) UNIQUE KEY `username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1; ) ENGINE=InnoDB DEFAULT CHARSET=latin1;


+ 2
- 1
sqlite.sql View File

@@ -214,7 +214,8 @@ CREATE TABLE IF NOT EXISTS `users` (
username TEXT NOT NULL UNIQUE, username TEXT NOT NULL UNIQUE,
password TEXT NOT NULL, password TEXT NOT NULL,
email TEXT DEFAULT NULL, email TEXT DEFAULT NULL,
created DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
created DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
suspended INTEGER NOT NULL DEFAULT 0
); );


-- -------------------------------------------------------- -- --------------------------------------------------------


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

@@ -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();


+ 5
- 1
templates/pad.tmpl View File

@@ -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 currently suspended, posting is disabled.");
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.");


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

@@ -11,12 +11,17 @@
<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">
<a
href="/admin/user/{{.Username}}#status"
title="View or change account status">{{if .Suspended}}suspended{{else}}active{{end}}</a></td>
</tr> </tr>
{{end}} {{end}}
</table> </table>


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

@@ -7,6 +7,24 @@ 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;
}
}
</style> </style>
<div class="snug content-container"> <div class="snug content-container">
{{template "admin-header" .}} {{template "admin-header" .}}
@@ -38,6 +56,20 @@ 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}}" method="POST">
<a id="status"/>
<th>Status</th>
{{if .User.Suspended}}
<td class="active-suspend"><p>User is currently Suspended</p><input type="submit" value="Activate"/></td>
{{else}}
<td class="active-suspend">
<p>User is currently Active</p>
<input class="danger" type="submit" value="Suspend" {{if .User.IsAdmin}}disabled{{end}}/>
</td>
{{end}}
</form>
</tr>
</table> </table>


<h2>Blogs</h2> <h2>Blogs</h2>


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

@@ -12,6 +12,12 @@ h3 { font-weight: normal; }
{{range .Flashes}}<li class="urgent">{{.}}</li>{{end}} {{range .Flashes}}<li class="urgent">{{.}}</li>{{end}}
</ul>{{end}} </ul>{{end}}


{{if .Suspended}}
<div class="alert info">
<p>This account is currently suspended.</p>
<p>Please contact the instance administrator to discuss reactivation.</p>
</div>
{{end}}
{{ if .IsLogOut }} {{ if .IsLogOut }}
<div class="alert info"> <div class="alert info">
<p class="introduction">Please add an <strong>email address</strong> and/or <strong>passphrase</strong> so you can log in again later.</p> <p class="introduction">Please add an <strong>email address</strong> and/or <strong>passphrase</strong> so you can log in again later.</p>


+ 1
- 0
users.go View File

@@ -59,6 +59,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"`
Suspended bool `json:"suspended"`


clearEmail string `json:"email"` clearEmail string `json:"email"`
} }


+ 10
- 1
webfinger.go View File

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


import ( import (
"net/http"

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


type wfResolver struct { 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) 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…
Cancel
Save