Add account suspension featurestags/v0.11.0^0
@@ -750,14 +750,20 @@ func viewArticles(app *App, u *User, w http.ResponseWriter, r *http.Request) err | |||
log.Error("unable to fetch collections: %v", err) | |||
} | |||
suspended, err := app.db.IsUserSuspended(u.ID) | |||
if err != nil { | |||
log.Error("view articles: %v", err) | |||
} | |||
d := struct { | |||
*UserPage | |||
AnonymousPosts *[]PublicPost | |||
Collections *[]Collection | |||
Suspended bool | |||
}{ | |||
UserPage: NewUserPage(app, r, u, u.Username+"'s Posts", f), | |||
AnonymousPosts: p, | |||
Collections: c, | |||
Suspended: suspended, | |||
} | |||
d.UserPage.SetMessaging(u) | |||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") | |||
@@ -779,6 +785,11 @@ func viewCollections(app *App, u *User, w http.ResponseWriter, r *http.Request) | |||
uc, _ := app.db.GetUserCollectionCount(u.ID) | |||
// TODO: handle any errors | |||
suspended, err := app.db.IsUserSuspended(u.ID) | |||
if err != nil { | |||
log.Error("view collections %v", err) | |||
return fmt.Errorf("view collections: %v", err) | |||
} | |||
d := struct { | |||
*UserPage | |||
Collections *[]Collection | |||
@@ -786,11 +797,13 @@ func viewCollections(app *App, u *User, w http.ResponseWriter, r *http.Request) | |||
UsedCollections, TotalCollections int | |||
NewBlogsDisabled bool | |||
Suspended bool | |||
}{ | |||
UserPage: NewUserPage(app, r, u, u.Username+"'s Blogs", f), | |||
Collections: c, | |||
UsedCollections: int(uc), | |||
NewBlogsDisabled: !app.cfg.App.CanCreateBlogs(uc), | |||
Suspended: suspended, | |||
} | |||
d.UserPage.SetMessaging(u) | |||
showUserPage(w, "collections", d) | |||
@@ -808,13 +821,20 @@ func viewEditCollection(app *App, u *User, w http.ResponseWriter, r *http.Reques | |||
return ErrCollectionNotFound | |||
} | |||
suspended, err := app.db.IsUserSuspended(u.ID) | |||
if err != nil { | |||
log.Error("view edit collection %v", err) | |||
return fmt.Errorf("view edit collection: %v", err) | |||
} | |||
flashes, _ := getSessionFlashes(app, w, r, nil) | |||
obj := struct { | |||
*UserPage | |||
*Collection | |||
Suspended bool | |||
}{ | |||
UserPage: NewUserPage(app, r, u, "Edit "+c.DisplayTitle(), flashes), | |||
Collection: c, | |||
Suspended: suspended, | |||
} | |||
showUserPage(w, "collection", obj) | |||
@@ -976,17 +996,24 @@ func viewStats(app *App, u *User, w http.ResponseWriter, r *http.Request) error | |||
titleStats = c.DisplayTitle() + " " | |||
} | |||
suspended, err := app.db.IsUserSuspended(u.ID) | |||
if err != nil { | |||
log.Error("view stats: %v", err) | |||
return err | |||
} | |||
obj := struct { | |||
*UserPage | |||
VisitsBlog string | |||
Collection *Collection | |||
TopPosts *[]PublicPost | |||
APFollowers int | |||
Suspended bool | |||
}{ | |||
UserPage: NewUserPage(app, r, u, titleStats+"Stats", flashes), | |||
VisitsBlog: alias, | |||
Collection: c, | |||
TopPosts: topPosts, | |||
Suspended: suspended, | |||
} | |||
if app.cfg.App.Federation { | |||
folls, err := app.db.GetAPFollowers(c) | |||
@@ -1017,14 +1044,16 @@ func viewSettings(app *App, u *User, w http.ResponseWriter, r *http.Request) err | |||
obj := struct { | |||
*UserPage | |||
Email string | |||
HasPass bool | |||
IsLogOut bool | |||
Email string | |||
HasPass bool | |||
IsLogOut bool | |||
Suspended bool | |||
}{ | |||
UserPage: NewUserPage(app, r, u, "Account Settings", flashes), | |||
Email: fullUser.EmailClear(app.keys), | |||
HasPass: passIsSet, | |||
IsLogOut: r.FormValue("logout") == "1", | |||
UserPage: NewUserPage(app, r, u, "Account Settings", flashes), | |||
Email: fullUser.EmailClear(app.keys), | |||
HasPass: passIsSet, | |||
IsLogOut: r.FormValue("logout") == "1", | |||
Suspended: fullUser.IsSilenced(), | |||
} | |||
showUserPage(w, "settings", obj) | |||
@@ -80,6 +80,14 @@ func handleFetchCollectionActivities(app *App, w http.ResponseWriter, r *http.Re | |||
if err != nil { | |||
return err | |||
} | |||
suspended, err := app.db.IsUserSuspended(c.OwnerID) | |||
if err != nil { | |||
log.Error("fetch collection activities: %v", err) | |||
return ErrInternalGeneral | |||
} | |||
if suspended { | |||
return ErrCollectionNotFound | |||
} | |||
c.hostName = app.cfg.App.Host | |||
p := c.PersonObject() | |||
@@ -105,6 +113,14 @@ func handleFetchCollectionOutbox(app *App, w http.ResponseWriter, r *http.Reques | |||
if err != nil { | |||
return err | |||
} | |||
suspended, err := app.db.IsUserSuspended(c.OwnerID) | |||
if err != nil { | |||
log.Error("fetch collection outbox: %v", err) | |||
return ErrInternalGeneral | |||
} | |||
if suspended { | |||
return ErrCollectionNotFound | |||
} | |||
c.hostName = app.cfg.App.Host | |||
if app.cfg.App.SingleUser { | |||
@@ -158,6 +174,14 @@ func handleFetchCollectionFollowers(app *App, w http.ResponseWriter, r *http.Req | |||
if err != nil { | |||
return err | |||
} | |||
suspended, err := app.db.IsUserSuspended(c.OwnerID) | |||
if err != nil { | |||
log.Error("fetch collection followers: %v", err) | |||
return ErrInternalGeneral | |||
} | |||
if suspended { | |||
return ErrCollectionNotFound | |||
} | |||
c.hostName = app.cfg.App.Host | |||
accountRoot := c.FederatedAccount() | |||
@@ -204,6 +228,14 @@ func handleFetchCollectionFollowing(app *App, w http.ResponseWriter, r *http.Req | |||
if err != nil { | |||
return err | |||
} | |||
suspended, err := app.db.IsUserSuspended(c.OwnerID) | |||
if err != nil { | |||
log.Error("fetch collection following: %v", err) | |||
return ErrInternalGeneral | |||
} | |||
if suspended { | |||
return ErrCollectionNotFound | |||
} | |||
c.hostName = app.cfg.App.Host | |||
accountRoot := c.FederatedAccount() | |||
@@ -238,6 +270,14 @@ func handleFetchCollectionInbox(app *App, w http.ResponseWriter, r *http.Request | |||
// TODO: return Reject? | |||
return err | |||
} | |||
suspended, err := app.db.IsUserSuspended(c.OwnerID) | |||
if err != nil { | |||
log.Error("fetch collection inbox: %v", err) | |||
return ErrInternalGeneral | |||
} | |||
if suspended { | |||
return ErrCollectionNotFound | |||
} | |||
c.hostName = app.cfg.App.Host | |||
if debugging { | |||
@@ -241,12 +241,37 @@ func handleViewAdminUser(app *App, u *User, w http.ResponseWriter, r *http.Reque | |||
return nil | |||
} | |||
func handleAdminToggleUserStatus(app *App, u *User, w http.ResponseWriter, r *http.Request) error { | |||
vars := mux.Vars(r) | |||
username := vars["username"] | |||
if username == "" { | |||
return impart.HTTPError{http.StatusFound, "/admin/users"} | |||
} | |||
user, err := app.db.GetUserForAuth(username) | |||
if err != nil { | |||
log.Error("failed to get user: %v", err) | |||
return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get user from username: %v", err)} | |||
} | |||
if user.IsSilenced() { | |||
err = app.db.SetUserStatus(user.ID, UserActive) | |||
} else { | |||
err = app.db.SetUserStatus(user.ID, UserSilenced) | |||
} | |||
if err != nil { | |||
log.Error("toggle user suspended: %v", err) | |||
return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not toggle user status: %v")} | |||
} | |||
return impart.HTTPError{http.StatusFound, fmt.Sprintf("/admin/user/%s#status", username)} | |||
} | |||
func handleAdminResetUserPass(app *App, u *User, w http.ResponseWriter, r *http.Request) error { | |||
vars := mux.Vars(r) | |||
username := vars["username"] | |||
if username == "" { | |||
return impart.HTTPError{http.StatusFound, "/admin/users"} | |||
} | |||
// Generate new random password since none supplied | |||
pass := passgen.NewWordish() | |||
hashedPass, err := auth.HashPass([]byte(pass)) | |||
@@ -71,6 +71,7 @@ type ( | |||
CurrentPage int | |||
TotalPages int | |||
Format *CollectionFormat | |||
Suspended bool | |||
} | |||
SubmittedCollection struct { | |||
// Data used for updating a given collection | |||
@@ -379,6 +380,7 @@ func newCollection(app *App, w http.ResponseWriter, r *http.Request) error { | |||
} | |||
var userID int64 | |||
var err error | |||
if reqJSON && !c.Web { | |||
accessToken = r.Header.Get("Authorization") | |||
if accessToken == "" { | |||
@@ -395,6 +397,14 @@ func newCollection(app *App, w http.ResponseWriter, r *http.Request) error { | |||
} | |||
userID = u.ID | |||
} | |||
suspended, err := app.db.IsUserSuspended(userID) | |||
if err != nil { | |||
log.Error("new collection: %v", err) | |||
return ErrInternalGeneral | |||
} | |||
if suspended { | |||
return ErrUserSuspended | |||
} | |||
if !author.IsValidUsername(app.cfg, c.Alias) { | |||
return impart.HTTPError{http.StatusPreconditionFailed, "Collection alias isn't valid."} | |||
@@ -477,6 +487,7 @@ func fetchCollection(app *App, w http.ResponseWriter, r *http.Request) error { | |||
res.Owner = u | |||
} | |||
} | |||
// TODO: check suspended | |||
app.db.GetPostsCount(res, isCollOwner) | |||
// Strip non-public information | |||
res.Collection.ForPublic() | |||
@@ -725,9 +736,14 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro | |||
if c == nil || err != nil { | |||
return err | |||
} | |||
c.hostName = app.cfg.App.Host | |||
suspended, err := app.db.IsUserSuspended(c.OwnerID) | |||
if err != nil { | |||
log.Error("view collection: %v", err) | |||
return ErrInternalGeneral | |||
} | |||
// Serve ActivityStreams data now, if requested | |||
if strings.Contains(r.Header.Get("Accept"), "application/activity+json") { | |||
ac := c.PersonObject() | |||
@@ -784,6 +800,10 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro | |||
log.Error("Error getting user for collection: %v", err) | |||
} | |||
} | |||
if !isOwner && suspended { | |||
return ErrCollectionNotFound | |||
} | |||
displayPage.Suspended = isOwner && suspended | |||
displayPage.Owner = owner | |||
coll.Owner = displayPage.Owner | |||
@@ -886,6 +906,10 @@ func handleViewCollectionTag(app *App, w http.ResponseWriter, r *http.Request) e | |||
log.Error("Error getting user for collection: %v", err) | |||
} | |||
} | |||
if !isOwner && u.IsSilenced() { | |||
return ErrCollectionNotFound | |||
} | |||
displayPage.Suspended = u.IsSilenced() | |||
displayPage.Owner = owner | |||
coll.Owner = displayPage.Owner | |||
// Add more data | |||
@@ -924,11 +948,10 @@ func existingCollection(app *App, w http.ResponseWriter, r *http.Request) error | |||
collAlias := vars["alias"] | |||
isWeb := r.FormValue("web") == "1" | |||
var u *User | |||
u := &User{} | |||
if reqJSON && !isWeb { | |||
// Ensure an access token was given | |||
accessToken := r.Header.Get("Authorization") | |||
u = &User{} | |||
u.ID = app.db.GetUserID(accessToken) | |||
if u.ID == -1 { | |||
return ErrBadAccessToken | |||
@@ -940,6 +963,16 @@ func existingCollection(app *App, w http.ResponseWriter, r *http.Request) error | |||
} | |||
} | |||
suspended, err := app.db.IsUserSuspended(u.ID) | |||
if err != nil { | |||
log.Error("existing collection: %v", err) | |||
return ErrInternalGeneral | |||
} | |||
if suspended { | |||
return ErrUserSuspended | |||
} | |||
if r.Method == "DELETE" { | |||
err := app.db.DeleteCollection(collAlias, u.ID) | |||
if err != nil { | |||
@@ -952,7 +985,6 @@ func existingCollection(app *App, w http.ResponseWriter, r *http.Request) error | |||
} | |||
c := SubmittedCollection{OwnerID: uint64(u.ID)} | |||
var err error | |||
if reqJSON { | |||
// Decode JSON request | |||
@@ -296,7 +296,7 @@ func (db *datastore) CreateCollection(cfg *config.Config, alias, title string, u | |||
func (db *datastore) GetUserByID(id int64) (*User, error) { | |||
u := &User{ID: id} | |||
err := db.QueryRow("SELECT username, password, email, created FROM users WHERE id = ?", id).Scan(&u.Username, &u.HashedPass, &u.Email, &u.Created) | |||
err := db.QueryRow("SELECT username, password, email, created, status FROM users WHERE id = ?", id).Scan(&u.Username, &u.HashedPass, &u.Email, &u.Created, &u.Status) | |||
switch { | |||
case err == sql.ErrNoRows: | |||
return nil, ErrUserNotFound | |||
@@ -308,6 +308,23 @@ func (db *datastore) GetUserByID(id int64) (*User, error) { | |||
return u, nil | |||
} | |||
// IsUserSuspended returns true if the user account associated with id is | |||
// currently suspended. | |||
func (db *datastore) IsUserSuspended(id int64) (bool, error) { | |||
u := &User{ID: id} | |||
err := db.QueryRow("SELECT status FROM users WHERE id = ?", id).Scan(&u.Status) | |||
switch { | |||
case err == sql.ErrNoRows: | |||
return false, fmt.Errorf("is user suspended: %v", ErrUserNotFound) | |||
case err != nil: | |||
log.Error("Couldn't SELECT user password: %v", err) | |||
return false, fmt.Errorf("is user suspended: %v", err) | |||
} | |||
return u.IsSilenced(), nil | |||
} | |||
// DoesUserNeedAuth returns true if the user hasn't provided any methods for | |||
// authenticating with the account, such a passphrase or email address. | |||
// Any errors are reported to admin and silently quashed, returning false as the | |||
@@ -347,7 +364,7 @@ func (db *datastore) IsUserPassSet(id int64) (bool, error) { | |||
func (db *datastore) GetUserForAuth(username string) (*User, error) { | |||
u := &User{Username: username} | |||
err := db.QueryRow("SELECT id, password, email, created FROM users WHERE username = ?", username).Scan(&u.ID, &u.HashedPass, &u.Email, &u.Created) | |||
err := db.QueryRow("SELECT id, password, email, created, status FROM users WHERE username = ?", username).Scan(&u.ID, &u.HashedPass, &u.Email, &u.Created, &u.Status) | |||
switch { | |||
case err == sql.ErrNoRows: | |||
// Check if they've entered the wrong, unnormalized username | |||
@@ -370,7 +387,7 @@ func (db *datastore) GetUserForAuth(username string) (*User, error) { | |||
func (db *datastore) GetUserForAuthByID(userID int64) (*User, error) { | |||
u := &User{ID: userID} | |||
err := db.QueryRow("SELECT id, password, email, created FROM users WHERE id = ?", u.ID).Scan(&u.ID, &u.HashedPass, &u.Email, &u.Created) | |||
err := db.QueryRow("SELECT id, password, email, created, status FROM users WHERE id = ?", u.ID).Scan(&u.ID, &u.HashedPass, &u.Email, &u.Created, &u.Status) | |||
switch { | |||
case err == sql.ErrNoRows: | |||
return nil, ErrUserNotFound | |||
@@ -1629,7 +1646,11 @@ func (db *datastore) GetMeStats(u *User) userMeStats { | |||
} | |||
func (db *datastore) GetTotalCollections() (collCount int64, err error) { | |||
err = db.QueryRow(`SELECT COUNT(*) FROM collections`).Scan(&collCount) | |||
err = db.QueryRow(` | |||
SELECT COUNT(*) | |||
FROM collections c | |||
LEFT JOIN users u ON u.id = c.owner_id | |||
WHERE u.status = 0`).Scan(&collCount) | |||
if err != nil { | |||
log.Error("Unable to fetch collections count: %v", err) | |||
} | |||
@@ -1637,7 +1658,11 @@ func (db *datastore) GetTotalCollections() (collCount int64, err error) { | |||
} | |||
func (db *datastore) GetTotalPosts() (postCount int64, err error) { | |||
err = db.QueryRow(`SELECT COUNT(*) FROM posts`).Scan(&postCount) | |||
err = db.QueryRow(` | |||
SELECT COUNT(*) | |||
FROM posts p | |||
LEFT JOIN users u ON u.id = p.owner_id | |||
WHERE u.status = 0`).Scan(&postCount) | |||
if err != nil { | |||
log.Error("Unable to fetch posts count: %v", err) | |||
} | |||
@@ -2359,17 +2384,17 @@ func (db *datastore) GetAllUsers(page uint) (*[]User, error) { | |||
limitStr = fmt.Sprintf("%d, %d", (page-1)*adminUsersPerPage, adminUsersPerPage) | |||
} | |||
rows, err := db.Query("SELECT id, username, created FROM users ORDER BY created DESC LIMIT " + limitStr) | |||
rows, err := db.Query("SELECT id, username, created, status FROM users ORDER BY created DESC LIMIT " + limitStr) | |||
if err != nil { | |||
log.Error("Failed selecting from posts: %v", err) | |||
return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve user posts."} | |||
log.Error("Failed selecting from users: %v", err) | |||
return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve all users."} | |||
} | |||
defer rows.Close() | |||
users := []User{} | |||
for rows.Next() { | |||
u := User{} | |||
err = rows.Scan(&u.ID, &u.Username, &u.Created) | |||
err = rows.Scan(&u.ID, &u.Username, &u.Created, &u.Status) | |||
if err != nil { | |||
log.Error("Failed scanning GetAllUsers() row: %v", err) | |||
break | |||
@@ -2406,6 +2431,15 @@ func (db *datastore) GetUserLastPostTime(id int64) (*time.Time, error) { | |||
return &t, nil | |||
} | |||
// SetUserStatus changes a user's status in the database. see Users.UserStatus | |||
func (db *datastore) SetUserStatus(id int64, status UserStatus) error { | |||
_, err := db.Exec("UPDATE users SET status = ? WHERE id = ?", status, id) | |||
if err != nil { | |||
return fmt.Errorf("failed to update user status: %v", err) | |||
} | |||
return nil | |||
} | |||
func (db *datastore) GetCollectionLastPostTime(id int64) (*time.Time, error) { | |||
var t time.Time | |||
err := db.QueryRow("SELECT created FROM posts WHERE collection_id = ? ORDER BY created DESC LIMIT 1", id).Scan(&t) | |||
@@ -11,8 +11,9 @@ | |||
package writefreely | |||
import ( | |||
"github.com/writeas/impart" | |||
"net/http" | |||
"github.com/writeas/impart" | |||
) | |||
// Commonly returned HTTP errors | |||
@@ -46,6 +47,8 @@ var ( | |||
ErrUserNotFound = impart.HTTPError{http.StatusNotFound, "User doesn't exist."} | |||
ErrUserNotFoundEmail = impart.HTTPError{http.StatusNotFound, "Please enter your username instead of your email address."} | |||
ErrUserSuspended = impart.HTTPError{http.StatusForbidden, "Account is silenced."} | |||
) | |||
// Post operation errors | |||
@@ -12,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() { | |||
@@ -78,6 +78,10 @@ func handleCreateUserInvite(app *App, u *User, w http.ResponseWriter, r *http.Re | |||
muVal := r.FormValue("uses") | |||
expVal := r.FormValue("expires") | |||
if u.IsSilenced() { | |||
return ErrUserSuspended | |||
} | |||
var err error | |||
var maxUses int | |||
if muVal != "0" { | |||
@@ -516,10 +516,17 @@ abbr { | |||
body#collection article p, body#subpage article p { | |||
.article-p; | |||
} | |||
pre, body#post article, body#collection article, body#subpage article, body#subpage #wrapper h1 { | |||
pre, body#post article, #post .alert, #subpage .alert, body#collection article, body#subpage article, body#subpage #wrapper h1 { | |||
max-width: 40rem; | |||
margin: 0 auto; | |||
} | |||
#collection header .alert, #post .alert, #subpage .alert { | |||
margin-bottom: 1em; | |||
p { | |||
text-align: left; | |||
line-height: 1.4; | |||
} | |||
} | |||
textarea, pre, body#post article, body#collection article p { | |||
&.norm, &.sans, &.wrap { | |||
line-height: 1.4em; | |||
@@ -13,6 +13,7 @@ package migrations | |||
import ( | |||
"database/sql" | |||
"github.com/writeas/web-core/log" | |||
) | |||
@@ -57,6 +58,7 @@ func (m *migration) Migrate(db *datastore) error { | |||
var migrations = []Migration{ | |||
New("support user invites", supportUserInvites), // -> V1 (v0.8.0) | |||
New("support dynamic instance pages", supportInstancePages), // V1 -> V2 (v0.9.0) | |||
New("support users suspension", supportUserStatus), // V2 -> V3 (v0.11.0) | |||
} | |||
// CurrentVer returns the current migration version the application is on | |||
@@ -0,0 +1,29 @@ | |||
/* | |||
* Copyright © 2019 A Bunch Tell LLC. | |||
* | |||
* This file is part of WriteFreely. | |||
* | |||
* WriteFreely is free software: you can redistribute it and/or modify | |||
* it under the terms of the GNU Affero General Public License, included | |||
* in the LICENSE file in this source code package. | |||
*/ | |||
package migrations | |||
func supportUserStatus(db *datastore) error { | |||
t, err := db.Begin() | |||
_, err = t.Exec(`ALTER TABLE users ADD COLUMN status ` + db.typeInt() + ` DEFAULT '0' NOT NULL`) | |||
if err != nil { | |||
t.Rollback() | |||
return err | |||
} | |||
err = t.Commit() | |||
if err != nil { | |||
t.Rollback() | |||
return err | |||
} | |||
return nil | |||
} |
@@ -35,9 +35,10 @@ func handleViewPad(app *App, w http.ResponseWriter, r *http.Request) error { | |||
} | |||
appData := &struct { | |||
page.StaticPage | |||
Post *RawPost | |||
User *User | |||
Blogs *[]Collection | |||
Post *RawPost | |||
User *User | |||
Blogs *[]Collection | |||
Suspended bool | |||
Editing bool // True if we're modifying an existing post | |||
EditCollection *Collection // Collection of the post we're editing, if any | |||
@@ -52,6 +53,10 @@ func handleViewPad(app *App, w http.ResponseWriter, r *http.Request) error { | |||
if err != nil { | |||
log.Error("Unable to get user's blogs for Pad: %v", err) | |||
} | |||
appData.Suspended, err = app.db.IsUserSuspended(appData.User.ID) | |||
if err != nil { | |||
log.Error("Unable to get users suspension status for Pad: %v", err) | |||
} | |||
} | |||
padTmpl := app.cfg.App.Editor | |||
@@ -119,12 +124,18 @@ func handleViewMeta(app *App, w http.ResponseWriter, r *http.Request) error { | |||
EditCollection *Collection // Collection of the post we're editing, if any | |||
Flashes []string | |||
NeedsToken bool | |||
Suspended bool | |||
}{ | |||
StaticPage: pageForReq(app, r), | |||
Post: &RawPost{Font: "norm"}, | |||
User: getUserSession(app, r), | |||
} | |||
var err error | |||
appData.Suspended, err = app.db.IsUserSuspended(appData.User.ID) | |||
if err != nil { | |||
log.Error("view meta: get user suspended status: %v", err) | |||
return ErrInternalGeneral | |||
} | |||
if action == "" && slug == "" { | |||
return ErrPostNotFound | |||
@@ -381,6 +381,12 @@ func handleViewPost(app *App, w http.ResponseWriter, r *http.Request) error { | |||
} | |||
} | |||
suspended, err := app.db.IsUserSuspended(ownerID.Int64) | |||
if err != nil { | |||
log.Error("view post: %v", err) | |||
return ErrInternalGeneral | |||
} | |||
// Check if post has been unpublished | |||
if content == "" { | |||
gone = true | |||
@@ -428,9 +434,10 @@ func handleViewPost(app *App, w http.ResponseWriter, r *http.Request) error { | |||
page := struct { | |||
*AnonymousPost | |||
page.StaticPage | |||
Username string | |||
IsOwner bool | |||
SiteURL string | |||
Username string | |||
IsOwner bool | |||
SiteURL string | |||
Suspended bool | |||
}{ | |||
AnonymousPost: post, | |||
StaticPage: pageForReq(app, r), | |||
@@ -441,6 +448,10 @@ func handleViewPost(app *App, w http.ResponseWriter, r *http.Request) error { | |||
page.IsOwner = ownerID.Valid && ownerID.Int64 == u.ID | |||
} | |||
if !page.IsOwner && suspended { | |||
return ErrPostNotFound | |||
} | |||
page.Suspended = suspended | |||
err = templates["post"].ExecuteTemplate(w, "post", page) | |||
if err != nil { | |||
log.Error("Post template execute error: %v", err) | |||
@@ -497,6 +508,15 @@ func newPost(app *App, w http.ResponseWriter, r *http.Request) error { | |||
} else { | |||
userID = app.db.GetUserID(accessToken) | |||
} | |||
suspended, err := app.db.IsUserSuspended(userID) | |||
if err != nil { | |||
log.Error("new post: %v", err) | |||
return ErrInternalGeneral | |||
} | |||
if suspended { | |||
return ErrUserSuspended | |||
} | |||
if userID == -1 { | |||
return ErrNotLoggedIn | |||
} | |||
@@ -509,7 +529,7 @@ func newPost(app *App, w http.ResponseWriter, r *http.Request) error { | |||
var p *SubmittedPost | |||
if reqJSON { | |||
decoder := json.NewDecoder(r.Body) | |||
err := decoder.Decode(&p) | |||
err = decoder.Decode(&p) | |||
if err != nil { | |||
log.Error("Couldn't parse new post JSON request: %v\n", err) | |||
return ErrBadJSON | |||
@@ -555,7 +575,6 @@ func newPost(app *App, w http.ResponseWriter, r *http.Request) error { | |||
var newPost *PublicPost = &PublicPost{} | |||
var coll *Collection | |||
var err error | |||
if accessToken != "" { | |||
newPost, err = app.db.CreateOwnedPost(p, accessToken, collAlias, app.cfg.App.Host) | |||
} else { | |||
@@ -663,6 +682,15 @@ func existingPost(app *App, w http.ResponseWriter, r *http.Request) error { | |||
} | |||
} | |||
suspended, err := app.db.IsUserSuspended(userID) | |||
if err != nil { | |||
log.Error("existing post: %v", err) | |||
return ErrInternalGeneral | |||
} | |||
if suspended { | |||
return ErrUserSuspended | |||
} | |||
// Modify post struct | |||
p.ID = postID | |||
@@ -857,11 +885,20 @@ func addPost(app *App, w http.ResponseWriter, r *http.Request) error { | |||
ownerID = u.ID | |||
} | |||
suspended, err := app.db.IsUserSuspended(ownerID) | |||
if err != nil { | |||
log.Error("add post: %v", err) | |||
return ErrInternalGeneral | |||
} | |||
if suspended { | |||
return ErrUserSuspended | |||
} | |||
// Parse claimed posts in format: | |||
// [{"id": "...", "token": "..."}] | |||
var claims *[]ClaimPostRequest | |||
decoder := json.NewDecoder(r.Body) | |||
err := decoder.Decode(&claims) | |||
err = decoder.Decode(&claims) | |||
if err != nil { | |||
return ErrBadJSONArray | |||
} | |||
@@ -951,13 +988,22 @@ func pinPost(app *App, w http.ResponseWriter, r *http.Request) error { | |||
userID = u.ID | |||
} | |||
suspended, err := app.db.IsUserSuspended(userID) | |||
if err != nil { | |||
log.Error("pin post: %v", err) | |||
return ErrInternalGeneral | |||
} | |||
if suspended { | |||
return ErrUserSuspended | |||
} | |||
// Parse request | |||
var posts []struct { | |||
ID string `json:"id"` | |||
Position int64 `json:"position"` | |||
} | |||
decoder := json.NewDecoder(r.Body) | |||
err := decoder.Decode(&posts) | |||
err = decoder.Decode(&posts) | |||
if err != nil { | |||
return ErrBadJSONArray | |||
} | |||
@@ -993,6 +1039,7 @@ func pinPost(app *App, w http.ResponseWriter, r *http.Request) error { | |||
func fetchPost(app *App, w http.ResponseWriter, r *http.Request) error { | |||
var collID int64 | |||
var ownerID int64 | |||
var coll *Collection | |||
var err error | |||
vars := mux.Vars(r) | |||
@@ -1008,12 +1055,22 @@ func fetchPost(app *App, w http.ResponseWriter, r *http.Request) error { | |||
return err | |||
} | |||
collID = coll.ID | |||
ownerID = coll.OwnerID | |||
} | |||
p, err := app.db.GetPost(vars["post"], collID) | |||
if err != nil { | |||
return err | |||
} | |||
suspended, err := app.db.IsUserSuspended(ownerID) | |||
if err != nil { | |||
log.Error("fetch post: %v", err) | |||
return ErrInternalGeneral | |||
} | |||
if suspended { | |||
return ErrPostNotFound | |||
} | |||
p.extractData() | |||
@@ -1275,6 +1332,12 @@ func viewCollectionPost(app *App, w http.ResponseWriter, r *http.Request) error | |||
} | |||
c.hostName = app.cfg.App.Host | |||
suspended, err := app.db.IsUserSuspended(c.OwnerID) | |||
if err != nil { | |||
log.Error("view collection post: %v", err) | |||
return ErrInternalGeneral | |||
} | |||
// Check collection permissions | |||
if c.IsPrivate() && (u == nil || u.ID != c.OwnerID) { | |||
return ErrPostNotFound | |||
@@ -1327,10 +1390,13 @@ Are you sure it was ever here?`, | |||
return err | |||
} | |||
} | |||
p.IsOwner = owner != nil && p.OwnerID.Valid && owner.ID == p.OwnerID.Int64 | |||
p.IsOwner = owner != nil && p.OwnerID.Valid && u.ID == p.OwnerID.Int64 | |||
p.Collection = coll | |||
p.IsTopLevel = app.cfg.App.SingleUser | |||
if !p.IsOwner && suspended { | |||
return ErrPostNotFound | |||
} | |||
// Check if post has been unpublished | |||
if p.Content == "" && p.Title.String == "" { | |||
return impart.HTTPError{http.StatusGone, "Post was unpublished."} | |||
@@ -1380,12 +1446,14 @@ Are you sure it was ever here?`, | |||
IsFound bool | |||
IsAdmin bool | |||
CanInvite bool | |||
Suspended bool | |||
}{ | |||
PublicPost: p, | |||
StaticPage: pageForReq(app, r), | |||
IsOwner: cr.isCollOwner, | |||
IsCustomDomain: cr.isCustomDomain, | |||
IsFound: postFound, | |||
Suspended: suspended, | |||
} | |||
tp.IsAdmin = u != nil && u.IsAdmin() | |||
tp.CanInvite = canUserInvite(app.cfg, tp.IsAdmin) | |||
@@ -13,6 +13,12 @@ package writefreely | |||
import ( | |||
"database/sql" | |||
"fmt" | |||
"html/template" | |||
"math" | |||
"net/http" | |||
"strconv" | |||
"time" | |||
. "github.com/gorilla/feeds" | |||
"github.com/gorilla/mux" | |||
stripmd "github.com/writeas/go-strip-markdown" | |||
@@ -20,11 +26,6 @@ import ( | |||
"github.com/writeas/web-core/log" | |||
"github.com/writeas/web-core/memo" | |||
"github.com/writeas/writefreely/page" | |||
"html/template" | |||
"math" | |||
"net/http" | |||
"strconv" | |||
"time" | |||
) | |||
const ( | |||
@@ -69,7 +70,8 @@ func (app *App) FetchPublicPosts() (interface{}, error) { | |||
rows, err := app.db.Query(`SELECT p.id, alias, c.title, p.slug, p.title, p.content, p.text_appearance, p.language, p.rtl, p.created, p.updated | |||
FROM collections c | |||
LEFT JOIN posts p ON p.collection_id = c.id | |||
WHERE c.privacy = 1 AND (p.created >= ` + app.db.dateSub(3, "month") + ` AND p.created <= ` + app.db.now() + ` AND pinned_position IS NULL) | |||
LEFT JOIN users u ON u.id = p.owner_id | |||
WHERE c.privacy = 1 AND (p.created >= ` + app.db.dateSub(3, "month") + ` AND p.created <= ` + app.db.now() + ` AND pinned_position IS NULL) AND u.status = 0 | |||
ORDER BY p.created DESC`) | |||
if err != nil { | |||
log.Error("Failed selecting from posts: %v", err) | |||
@@ -144,6 +144,7 @@ func InitRoutes(apper Apper, r *mux.Router) *mux.Router { | |||
write.HandleFunc("/admin", handler.Admin(handleViewAdminDash)).Methods("GET") | |||
write.HandleFunc("/admin/users", handler.Admin(handleViewAdminUsers)).Methods("GET") | |||
write.HandleFunc("/admin/user/{username}", handler.Admin(handleViewAdminUser)).Methods("GET") | |||
write.HandleFunc("/admin/user/{username}/status", handler.Admin(handleAdminToggleUserStatus)).Methods("POST") | |||
write.HandleFunc("/admin/user/{username}/passphrase", handler.Admin(handleAdminResetUserPass)).Methods("POST") | |||
write.HandleFunc("/admin/pages", handler.Admin(handleViewAdminPages)).Methods("GET") | |||
write.HandleFunc("/admin/page/{slug}", handler.Admin(handleViewAdminPage)).Methods("GET") | |||
@@ -11,10 +11,6 @@ | |||
package writefreely | |||
import ( | |||
"github.com/dustin/go-humanize" | |||
"github.com/writeas/web-core/l10n" | |||
"github.com/writeas/web-core/log" | |||
"github.com/writeas/writefreely/config" | |||
"html/template" | |||
"io" | |||
"io/ioutil" | |||
@@ -22,6 +18,11 @@ import ( | |||
"os" | |||
"path/filepath" | |||
"strings" | |||
"github.com/dustin/go-humanize" | |||
"github.com/writeas/web-core/l10n" | |||
"github.com/writeas/web-core/log" | |||
"github.com/writeas/writefreely/config" | |||
) | |||
var ( | |||
@@ -63,6 +64,7 @@ func initTemplate(parentDir, name string) { | |||
filepath.Join(parentDir, templatesDir, name+".tmpl"), | |||
filepath.Join(parentDir, templatesDir, "include", "footer.tmpl"), | |||
filepath.Join(parentDir, templatesDir, "base.tmpl"), | |||
filepath.Join(parentDir, templatesDir, "user", "include", "suspended.tmpl"), | |||
} | |||
if name == "collection" || name == "collection-tags" || name == "chorus-collection" { | |||
// These pages list out collection posts, so we also parse templatesDir + "include/posts.tmpl" | |||
@@ -86,6 +88,7 @@ func initPage(parentDir, path, key string) { | |||
path, | |||
filepath.Join(parentDir, templatesDir, "include", "footer.tmpl"), | |||
filepath.Join(parentDir, templatesDir, "base.tmpl"), | |||
filepath.Join(parentDir, templatesDir, "user", "include", "suspended.tmpl"), | |||
)) | |||
} | |||
@@ -98,6 +101,7 @@ func initUserPage(parentDir, path, key string) { | |||
path, | |||
filepath.Join(parentDir, templatesDir, "user", "include", "header.tmpl"), | |||
filepath.Join(parentDir, templatesDir, "user", "include", "footer.tmpl"), | |||
filepath.Join(parentDir, templatesDir, "user", "include", "suspended.tmpl"), | |||
)) | |||
} | |||
@@ -65,6 +65,9 @@ article time.dt-published { | |||
{{template "user-navigation" .}} | |||
{{if .Suspended}} | |||
{{template "user-suspended"}} | |||
{{end}} | |||
<article id="post-body" class="{{.Font}} h-entry">{{if .IsScheduled}}<p class="badge">Scheduled</p>{{end}}{{if .Title.String}}<h2 id="title" class="p-name">{{.FormattedDisplayTitle}}</h2>{{end}}{{/* TODO: check format: if .Collection.Format.ShowDates*/}}<time class="dt-published" datetime="{{.Created}}" pubdate itemprop="datePublished" content="{{.Created}}">{{.DisplayDate}}</time><div class="e-content">{{.HTMLContent}}</div></article> | |||
{{ if .Collection.ShowFooterBranding }} | |||
@@ -61,6 +61,9 @@ body#collection header nav.tabs a:first-child { | |||
<body id="collection" itemscope itemtype="http://schema.org/WebPage"> | |||
{{template "user-navigation" .}} | |||
{{if .Suspended}} | |||
{{template "user-suspended"}} | |||
{{end}} | |||
<header> | |||
<h1 dir="{{.Direction}}" id="blog-title"><a href="/{{if .IsTopLevel}}{{else}}{{.Prefix}}{{.Alias}}/{{end}}" class="h-card p-author u-url" rel="me author">{{.DisplayTitle}}</a></h1> | |||
{{if .Description}}<p class="description p-note">{{.Description}}</p>{{end}} | |||
@@ -59,6 +59,9 @@ | |||
</nav> | |||
</header> | |||
{{if .Suspended}} | |||
{{template "user-suspended"}} | |||
{{end}} | |||
<article id="post-body" class="{{.Font}} h-entry {{if not .IsFound}}error-page{{end}}">{{if .IsScheduled}}<p class="badge">Scheduled</p>{{end}}{{if .Title.String}}<h2 id="title" class="p-name">{{.FormattedDisplayTitle}}</h2>{{end}}<div class="e-content">{{.HTMLContent}}</div></article> | |||
{{ if .Collection.ShowFooterBranding }} | |||
@@ -53,6 +53,9 @@ | |||
</nav> | |||
</header> | |||
{{if .Suspended}} | |||
{{template "user-suspended"}} | |||
{{end}} | |||
{{if .Posts}}<section id="wrapper" itemscope itemtype="http://schema.org/Blog">{{else}}<div id="wrapper">{{end}} | |||
<h1>{{.Tag}}</h1> | |||
{{template "posts" .}} | |||
@@ -62,6 +62,9 @@ | |||
</ul></nav>{{end}} | |||
<header> | |||
{{if .Suspended}} | |||
{{template "user-suspended"}} | |||
{{end}} | |||
<h1 dir="{{.Direction}}" id="blog-title">{{if .Posts}}{{else}}<span class="writeas-prefix"><a href="/">write.as</a></span> {{end}}<a href="/{{if .IsTopLevel}}{{else}}{{.Prefix}}{{.Alias}}/{{end}}" class="h-card p-author u-url" rel="me author">{{.DisplayTitle}}</a></h1> | |||
{{if .Description}}<p class="description p-note">{{.Description}}</p>{{end}} | |||
{{/*if not .Public/*}} | |||
@@ -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(); | |||
@@ -131,8 +131,12 @@ | |||
{{else}}var canPublish = true;{{end}} | |||
var publishing = false; | |||
var justPublished = false; | |||
var suspended = {{.Suspended}}; | |||
var publish = function(content, font) { | |||
if (suspended === true) { | |||
alert("Your account is silenced, so you can't publish or update posts."); | |||
return; | |||
} | |||
{{if and (and .Post.Id (not .Post.Slug)) (not .User)}} | |||
if (!token) { | |||
alert("You don't have permission to update this post."); | |||
@@ -25,6 +25,9 @@ | |||
</head> | |||
<body id="collection" itemscope itemtype="http://schema.org/WebPage"> | |||
{{if .Suspended}} | |||
{{template "user-supsended"}} | |||
{{end}} | |||
<header> | |||
<h1 dir="{{.Direction}}" id="blog-title"><a href="/{{.Alias}}/" class="h-card p-author u-url" rel="me author">{{.DisplayTitle}}</a></h1> | |||
</header> | |||
@@ -35,7 +35,6 @@ | |||
{{template "highlighting" .}} | |||
</head> | |||
<body id="post"> | |||
<header> | |||
<h1 dir="{{.Direction}}"><a href="/">{{.SiteName}}</a></h1> | |||
<nav> | |||
@@ -49,6 +48,10 @@ | |||
{{ end }} | |||
</nav> | |||
</header> | |||
{{if .Suspended}} | |||
{{template "user-suspended"}} | |||
{{end}} | |||
<article class="{{.Font}} h-entry">{{if .Title}}<h2 id="title" class="p-name">{{.Title}}</h2>{{end}}{{ if .IsPlainText }}<p id="post-body" class="e-content">{{.Content}}</p>{{ else }}<div id="post-body" class="e-content">{{.HTMLContent}}</div>{{ end }}</article> | |||
@@ -11,12 +11,14 @@ | |||
<th>User</th> | |||
<th>Joined</th> | |||
<th>Type</th> | |||
<th>Status</th> | |||
</tr> | |||
{{range .Users}} | |||
<tr> | |||
<td><a href="/admin/user/{{.Username}}">{{.Username}}</a></td> | |||
<td>{{.CreatedFriendly}}</td> | |||
<td style="text-align:center">{{if .IsAdmin}}Admin{{else}}User{{end}}</td> | |||
<td style="text-align:center">{{if .IsSilenced}}Silenced{{else}}Active{{end}}</td> | |||
</tr> | |||
{{end}} | |||
</table> | |||
@@ -7,6 +7,24 @@ table.classy th { | |||
h3 { | |||
font-weight: normal; | |||
} | |||
td.active-suspend { | |||
display: flex; | |||
align-items: center; | |||
} | |||
td.active-suspend > input[type="submit"] { | |||
margin-left: auto; | |||
margin-right: 5%; | |||
} | |||
@media only screen and (max-width: 500px) { | |||
td.active-suspend { | |||
flex-wrap: wrap; | |||
} | |||
td.active-suspend > input[type="submit"] { | |||
margin: auto; | |||
} | |||
} | |||
input.copy-text { | |||
text-align: center; | |||
font-size: 1.2em; | |||
@@ -52,6 +70,21 @@ input.copy-text { | |||
<td>{{if .LastPost}}{{.LastPost}}{{else}}Never{{end}}</td> | |||
</tr> | |||
<tr> | |||
<form action="/admin/user/{{.User.Username}}/status" method="POST" {{if not .User.IsSilenced}}onsubmit="return confirmSilence()"{{end}}> | |||
<a id="status"/> | |||
<th>Status</th> | |||
<td class="active-suspend"> | |||
{{if .User.IsSilenced}} | |||
<p>Silenced</p> | |||
<input type="submit" value="Unsilence"/> | |||
{{else}} | |||
<p>Active</p> | |||
<input class="danger" type="submit" value="Silence" {{if .User.IsAdmin}}disabled{{end}}/> | |||
{{end}} | |||
</td> | |||
</form> | |||
</tr> | |||
<tr> | |||
<th>Password</th> | |||
<td> | |||
{{if ne .Username .User.Username}} | |||
@@ -110,6 +143,10 @@ input.copy-text { | |||
</div> | |||
<script type="text/javascript"> | |||
function confirmSilence() { | |||
return confirm("Silence this user? They'll still be able to log in and access their posts, but no one else will be able to see them anymore. You can reverse this decision at any time."); | |||
} | |||
form = document.getElementById("reset-form"); | |||
form.addEventListener('submit', function(e) { | |||
e.preventDefault(); | |||
@@ -6,6 +6,9 @@ | |||
{{if .Flashes}}<ul class="errors"> | |||
{{range .Flashes}}<li class="urgent">{{.}}</li>{{end}} | |||
</ul>{{end}} | |||
{{if .Suspended}} | |||
{{template "user-suspended"}} | |||
{{end}} | |||
<h2 id="posts-header">drafts</h2> | |||
@@ -8,6 +8,9 @@ | |||
<div class="content-container snug"> | |||
<div id="overlay"></div> | |||
{{if .Suspended}} | |||
{{template "user-suspended"}} | |||
{{end}} | |||
<h2>Customize {{.DisplayTitle}} <a href="{{if .SingleUser}}/{{else}}/{{.Alias}}/{{end}}">view blog</a></h2> | |||
{{if .Flashes}}<ul class="errors"> | |||
@@ -7,6 +7,9 @@ | |||
{{range .Flashes}}<li class="urgent">{{.}}</li>{{end}} | |||
</ul>{{end}} | |||
{{if .Suspended}} | |||
{{template "user-suspended"}} | |||
{{end}} | |||
<h2>blogs</h2> | |||
<ul class="atoms collections"> | |||
{{range $i, $el := .Collections}}<li class="collection"><h3> | |||
@@ -0,0 +1,5 @@ | |||
{{define "user-suspended"}} | |||
<div class="alert info"> | |||
<p><strong>Your account has been silenced.</strong> You can still access all of your posts and blogs, but no one else can currently see them.</p> | |||
</div> | |||
{{end}} |
@@ -7,6 +7,9 @@ h3 { font-weight: normal; } | |||
.section > *:not(input) { font-size: 0.86em; } | |||
</style> | |||
<div class="content-container snug regular"> | |||
{{if .Suspended}} | |||
{{template "user-suspended"}} | |||
{{end}} | |||
<h2>{{if .IsLogOut}}Before you go...{{else}}Account Settings {{if .IsAdmin}}<a href="/admin">admin settings</a>{{end}}{{end}}</h2> | |||
{{if .Flashes}}<ul class="errors"> | |||
{{range .Flashes}}<li class="urgent">{{.}}</li>{{end}} | |||
@@ -17,6 +17,9 @@ td.none { | |||
</style> | |||
<div class="content-container snug"> | |||
{{if .Suspended}} | |||
{{template "user-suspended"}} | |||
{{end}} | |||
<h2 id="posts-header">{{if .Collection}}{{.Collection.DisplayTitle}} {{end}}Stats</h2> | |||
<p>Stats for all time.</p> | |||
@@ -19,6 +19,13 @@ import ( | |||
"github.com/writeas/writefreely/key" | |||
) | |||
type UserStatus int | |||
const ( | |||
UserActive = iota | |||
UserSilenced | |||
) | |||
type ( | |||
userCredentials struct { | |||
Alias string `json:"alias" schema:"alias"` | |||
@@ -59,6 +66,7 @@ type ( | |||
HasPass bool `json:"has_pass"` | |||
Email zero.String `json:"email"` | |||
Created time.Time `json:"created"` | |||
Status UserStatus `json:"status"` | |||
clearEmail string `json:"email"` | |||
} | |||
@@ -118,3 +126,7 @@ func (u *User) IsAdmin() bool { | |||
// TODO: get this from database | |||
return u.ID == 1 | |||
} | |||
func (u *User) IsSilenced() bool { | |||
return u.Status&UserSilenced != 0 | |||
} |
@@ -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 | |||