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 2 years ago
parent
commit
77f7b4a522
No known key found for this signature in database GPG Key ID: D6F1633A4F0903B8
  1. 29
      account.go
  2. 40
      activitypub.go
  3. 34
      admin.go
  4. 34
      collections.go
  5. 51
      database.go
  6. 5
      errors.go
  7. 14
      feed.go
  8. 13
      invites.go
  9. 2
      migrations/migrations.go
  10. 29
      migrations/v3.go
  11. 22
      pad.go
  12. 73
      posts.go
  13. 14
      read.go
  14. 8
      routes.go
  15. 1
      schema.sql
  16. 3
      sqlite.sql
  17. 4
      templates/edit-meta.tmpl
  18. 6
      templates/pad.tmpl
  19. 5
      templates/user/admin/users.tmpl
  20. 32
      templates/user/admin/view-user.tmpl
  21. 6
      templates/user/settings.tmpl
  22. 1
      users.go
  23. 11
      webfinger.go

29
account.go

@ -13,6 +13,13 @@ package writefreely
import (
"encoding/json"
"fmt"
"html/template"
"net/http"
"regexp"
"strings"
"sync"
"time"
"github.com/gorilla/mux"
"github.com/gorilla/sessions"
"github.com/guregu/null/zero"
@ -22,12 +29,6 @@ import (
"github.com/writeas/web-core/log"
"github.com/writeas/writefreely/author"
"github.com/writeas/writefreely/page"
"html/template"
"net/http"
"regexp"
"strings"
"sync"
"time"
)
type (
@ -1011,14 +1012,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.Suspended,
}
showUserPage(w, "settings", obj)

40
activitypub.go

@ -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 inbox: get owner: %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 inbox: get owner: %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 inbox: get owner: %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 inbox: get owner: %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: get owner: %v", err)
return ErrInternalGeneral
}
if suspended {
return ErrCollectionNotFound
}
c.hostName = app.cfg.App.Host
if debugging {

34
admin.go

@ -13,16 +13,17 @@ package writefreely
import (
"database/sql"
"fmt"
"net/http"
"runtime"
"strconv"
"time"
"github.com/gogits/gogs/pkg/tool"
"github.com/gorilla/mux"
"github.com/writeas/impart"
"github.com/writeas/web-core/auth"
"github.com/writeas/web-core/log"
"github.com/writeas/writefreely/config"
"net/http"
"runtime"
"strconv"
"time"
)
var (
@ -229,6 +230,31 @@ func handleViewAdminUser(app *App, u *User, w http.ResponseWriter, r *http.Reque
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 {
p := struct {
*UserPage

34
collections.go

@ -379,6 +379,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 +396,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: get user: %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."}
@ -724,6 +733,15 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro
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
if strings.Contains(r.Header.Get("Accept"), "application/activity+json") {
ac := c.PersonObject()
@ -824,6 +842,10 @@ func handleViewCollectionTag(app *App, w http.ResponseWriter, r *http.Request) e
return err
}
if u.Suspended {
return ErrCollectionNotFound
}
page := getCollectionPage(vars)
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 {
// 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
@ -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" {
err := app.db.DeleteCollection(collAlias, u.ID)
if err != nil {
@ -940,7 +971,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

51
database.go

@ -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, suspended FROM users WHERE id = ?", id).Scan(&u.Username, &u.HashedPass, &u.Email, &u.Created, &u.Suspended)
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 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
// 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, suspended FROM users WHERE username = ?", username).Scan(&u.ID, &u.HashedPass, &u.Email, &u.Created, &u.Suspended)
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, suspended FROM users WHERE id = ?", u.ID).Scan(&u.ID, &u.HashedPass, &u.Email, &u.Created, &u.Suspended)
switch {
case err == sql.ErrNoRows:
return nil, ErrUserNotFound
@ -1624,7 +1641,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.suspended = 0`).Scan(&collCount)
if err != nil {
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) {
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 {
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)
}
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 {
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.Suspended)
if err != nil {
log.Error("Failed scanning GetAllUsers() row: %v", err)
break
@ -2388,6 +2413,14 @@ func (db *datastore) GetUserLastPostTime(id int64) (*time.Time, error) {
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) {
var t time.Time
err := db.QueryRow("SELECT created FROM posts WHERE collection_id = ? ORDER BY created DESC LIMIT 1", id).Scan(&t)

5
errors.go

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

14
feed.go

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

13
invites.go

@ -12,15 +12,16 @@ package writefreely
import (
"database/sql"
"html/template"
"net/http"
"strconv"
"time"
"github.com/gorilla/mux"
"github.com/writeas/impart"
"github.com/writeas/nerds/store"
"github.com/writeas/web-core/log"
"github.com/writeas/writefreely/page"
"html/template"
"net/http"
"strconv"
"time"
)
type Invite struct {
@ -77,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.Suspended {
return ErrUserSuspended
}
var err error
var maxUses int
if muVal != "0" {

2
migrations/migrations.go

@ -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", supportUserSuspension), // V2 -> V3 ()
}
// CurrentVer returns the current migration version the application is on

29
migrations/v3.go

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

22
pad.go

@ -11,12 +11,13 @@
package writefreely
import (
"net/http"
"strings"
"github.com/gorilla/mux"
"github.com/writeas/impart"
"github.com/writeas/web-core/log"
"github.com/writeas/writefreely/page"
"net/http"
"strings"
)
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 {
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
@ -51,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
@ -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
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

73
posts.go

@ -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
if content == "" {
gone = true
@ -496,6 +506,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: get user: %v", err)
return ErrInternalGeneral
}
if suspended {
return ErrUserSuspended
}
if userID == -1 {
return ErrNotLoggedIn
}
@ -508,7 +527,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
@ -554,7 +573,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 {
@ -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
p.ID = postID
@ -856,11 +883,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: get user: %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
}
@ -950,13 +986,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: get user: %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
}
@ -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 {
var collID int64
var ownerID int64
var coll *Collection
var err error
vars := mux.Vars(r)
@ -1007,12 +1053,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: get owner: %v", err)
return ErrInternalGeneral
}
if suspended {
return ErrPostNotFound
}
p.extractData()
@ -1270,6 +1326,15 @@ 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: get owner: %v", err)
return ErrInternalGeneral
}
if suspended {
return ErrPostNotFound
}
// Check collection permissions
if c.IsPrivate() && (u == nil || u.ID != c.OwnerID) {
return ErrPostNotFound

14
read.go

@ -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 (
@ -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
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.suspended = 0
ORDER BY p.created DESC`)
if err != nil {
log.Error("Failed selecting from posts: %v", err)

8
routes.go

@ -11,13 +11,14 @@
package writefreely
import (
"net/http"
"path/filepath"
"strings"
"github.com/gorilla/mux"
"github.com/writeas/go-webfinger"
"github.com/writeas/web-core/log"
"github.com/writefreely/go-nodeinfo"
"net/http"
"path/filepath"
"strings"
)
// 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/users", handler.Admin(handleViewAdminUsers)).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/page/{slug}", handler.Admin(handleViewAdminPage)).Methods("GET")
write.HandleFunc("/admin/update/config", handler.AdminApper(handleAdminUpdateConfig)).Methods("POST")

1
schema.sql

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

3
sqlite.sql

@ -214,7 +214,8 @@ CREATE TABLE IF NOT EXISTS `users` (
username TEXT NOT NULL UNIQUE,
password TEXT NOT 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
templates/edit-meta.tmpl

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

6
templates/pad.tmpl

@ -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 currently suspended, posting is disabled.");
return;
}
{{if and (and .Post.Id (not .Post.Slug)) (not .User)}}
if (!token) {
alert("You don't have permission to update this post.");

5
templates/user/admin/users.tmpl

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

32
templates/user/admin/view-user.tmpl

@ -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;
}
}
</style>
<div class="snug content-container">
{{template "admin-header" .}}
@ -38,6 +56,20 @@ h3 {
<th>Last Post</th>
<td>{{if .LastPost}}{{.LastPost}}{{else}}Never{{end}}</td>
</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>
<h2>Blogs</h2>

6
templates/user/settings.tmpl

@ -12,6 +12,12 @@ h3 { font-weight: normal; }
{{range .Flashes}}<li class="urgent">{{.}}</li>{{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 }}
<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>

1
users.go

@ -59,6 +59,7 @@ type (
HasPass bool `json:"has_pass"`
Email zero.String `json:"email"`
Created time.Time `json:"created"`
Suspended bool `json:"suspended"`
clearEmail string `json:"email"`
}

11
webfinger.go

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