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
@@ -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) | ||||
@@ -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 { | ||||
@@ -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 | ||||
@@ -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 | ||||
@@ -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) | ||||
@@ -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,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() { | ||||
@@ -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" { | ||||
@@ -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 | ||||
@@ -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 | |||||
} |
@@ -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 | ||||
@@ -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 | ||||
@@ -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) | ||||
@@ -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") | ||||
@@ -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; | ||||
@@ -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 | |||||
); | ); | ||||
-- -------------------------------------------------------- | -- -------------------------------------------------------- | ||||
@@ -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(); | ||||
@@ -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."); | ||||
@@ -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> | ||||
@@ -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> | ||||
@@ -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> | ||||
@@ -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"` | ||||
} | } | ||||
@@ -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 | ||||