address PR comments

- update error messages to be correct
- move suspended message into template and include for other pages
- check suspended status on all relevant pages and show message if
logged in user is suspended.
- fix possible nil pointer error
- remove changes to db schema files
- add version comment to migration
- add UserStatus type with UserActive and UserSuspended
- change database table to use status column instead of suspended
- update toggle suspended handler to be toggle status in prep for
possible future inclusion of further user statuses
This commit is contained in:
Rob Loranger 2019-10-25 12:04:24 -07:00
parent 9873fc443f
commit f85f0751a3
No known key found for this signature in database
GPG Key ID: D6F1633A4F0903B8
30 changed files with 148 additions and 72 deletions

View File

@ -750,14 +750,20 @@ func viewArticles(app *App, u *User, w http.ResponseWriter, r *http.Request) err
log.Error("unable to fetch collections: %v", err) 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 { d := struct {
*UserPage *UserPage
AnonymousPosts *[]PublicPost AnonymousPosts *[]PublicPost
Collections *[]Collection Collections *[]Collection
Suspended bool
}{ }{
UserPage: NewUserPage(app, r, u, u.Username+"'s Posts", f), UserPage: NewUserPage(app, r, u, u.Username+"'s Posts", f),
AnonymousPosts: p, AnonymousPosts: p,
Collections: c, Collections: c,
Suspended: suspended,
} }
d.UserPage.SetMessaging(u) d.UserPage.SetMessaging(u)
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") 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) uc, _ := app.db.GetUserCollectionCount(u.ID)
// TODO: handle any errors // 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 { d := struct {
*UserPage *UserPage
Collections *[]Collection Collections *[]Collection
@ -786,11 +797,13 @@ func viewCollections(app *App, u *User, w http.ResponseWriter, r *http.Request)
UsedCollections, TotalCollections int UsedCollections, TotalCollections int
NewBlogsDisabled bool NewBlogsDisabled bool
Suspended bool
}{ }{
UserPage: NewUserPage(app, r, u, u.Username+"'s Blogs", f), UserPage: NewUserPage(app, r, u, u.Username+"'s Blogs", f),
Collections: c, Collections: c,
UsedCollections: int(uc), UsedCollections: int(uc),
NewBlogsDisabled: !app.cfg.App.CanCreateBlogs(uc), NewBlogsDisabled: !app.cfg.App.CanCreateBlogs(uc),
Suspended: suspended,
} }
d.UserPage.SetMessaging(u) d.UserPage.SetMessaging(u)
showUserPage(w, "collections", d) showUserPage(w, "collections", d)
@ -808,13 +821,20 @@ func viewEditCollection(app *App, u *User, w http.ResponseWriter, r *http.Reques
return ErrCollectionNotFound 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) flashes, _ := getSessionFlashes(app, w, r, nil)
obj := struct { obj := struct {
*UserPage *UserPage
*Collection *Collection
Suspended bool
}{ }{
UserPage: NewUserPage(app, r, u, "Edit "+c.DisplayTitle(), flashes), UserPage: NewUserPage(app, r, u, "Edit "+c.DisplayTitle(), flashes),
Collection: c, Collection: c,
Suspended: suspended,
} }
showUserPage(w, "collection", obj) showUserPage(w, "collection", obj)
@ -976,17 +996,24 @@ func viewStats(app *App, u *User, w http.ResponseWriter, r *http.Request) error
titleStats = c.DisplayTitle() + " " titleStats = c.DisplayTitle() + " "
} }
suspended, err := app.db.IsUserSuspended(u.ID)
if err != nil {
log.Error("view stats: %v", err)
return err
}
obj := struct { obj := struct {
*UserPage *UserPage
VisitsBlog string VisitsBlog string
Collection *Collection Collection *Collection
TopPosts *[]PublicPost TopPosts *[]PublicPost
APFollowers int APFollowers int
Suspended bool
}{ }{
UserPage: NewUserPage(app, r, u, titleStats+"Stats", flashes), UserPage: NewUserPage(app, r, u, titleStats+"Stats", flashes),
VisitsBlog: alias, VisitsBlog: alias,
Collection: c, Collection: c,
TopPosts: topPosts, TopPosts: topPosts,
Suspended: suspended,
} }
if app.cfg.App.Federation { if app.cfg.App.Federation {
folls, err := app.db.GetAPFollowers(c) folls, err := app.db.GetAPFollowers(c)
@ -1026,7 +1053,7 @@ func viewSettings(app *App, u *User, w http.ResponseWriter, r *http.Request) err
Email: fullUser.EmailClear(app.keys), Email: fullUser.EmailClear(app.keys),
HasPass: passIsSet, HasPass: passIsSet,
IsLogOut: r.FormValue("logout") == "1", IsLogOut: r.FormValue("logout") == "1",
Suspended: fullUser.Suspended, Suspended: fullUser.Status == UserSuspended,
} }
showUserPage(w, "settings", obj) showUserPage(w, "settings", obj)

View File

@ -82,7 +82,7 @@ func handleFetchCollectionActivities(app *App, w http.ResponseWriter, r *http.Re
} }
suspended, err := app.db.IsUserSuspended(c.OwnerID) suspended, err := app.db.IsUserSuspended(c.OwnerID)
if err != nil { if err != nil {
log.Error("fetch collection inbox: get owner: %v", err) log.Error("fetch collection activities: %v", err)
return ErrInternalGeneral return ErrInternalGeneral
} }
if suspended { if suspended {
@ -115,7 +115,7 @@ func handleFetchCollectionOutbox(app *App, w http.ResponseWriter, r *http.Reques
} }
suspended, err := app.db.IsUserSuspended(c.OwnerID) suspended, err := app.db.IsUserSuspended(c.OwnerID)
if err != nil { if err != nil {
log.Error("fetch collection inbox: get owner: %v", err) log.Error("fetch collection outbox: %v", err)
return ErrInternalGeneral return ErrInternalGeneral
} }
if suspended { if suspended {
@ -176,7 +176,7 @@ func handleFetchCollectionFollowers(app *App, w http.ResponseWriter, r *http.Req
} }
suspended, err := app.db.IsUserSuspended(c.OwnerID) suspended, err := app.db.IsUserSuspended(c.OwnerID)
if err != nil { if err != nil {
log.Error("fetch collection inbox: get owner: %v", err) log.Error("fetch collection followers: %v", err)
return ErrInternalGeneral return ErrInternalGeneral
} }
if suspended { if suspended {
@ -230,7 +230,7 @@ func handleFetchCollectionFollowing(app *App, w http.ResponseWriter, r *http.Req
} }
suspended, err := app.db.IsUserSuspended(c.OwnerID) suspended, err := app.db.IsUserSuspended(c.OwnerID)
if err != nil { if err != nil {
log.Error("fetch collection inbox: get owner: %v", err) log.Error("fetch collection following: %v", err)
return ErrInternalGeneral return ErrInternalGeneral
} }
if suspended { if suspended {
@ -272,7 +272,7 @@ func handleFetchCollectionInbox(app *App, w http.ResponseWriter, r *http.Request
} }
suspended, err := app.db.IsUserSuspended(c.OwnerID) suspended, err := app.db.IsUserSuspended(c.OwnerID)
if err != nil { if err != nil {
log.Error("fetch collection inbox: get owner: %v", err) log.Error("fetch collection inbox: %v", err)
return ErrInternalGeneral return ErrInternalGeneral
} }
if suspended { if suspended {

View File

@ -230,28 +230,27 @@ 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 { func handleAdminToggleUserStatus(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r) vars := mux.Vars(r)
username := vars["username"] username := vars["username"]
if username == "" { if username == "" {
return impart.HTTPError{http.StatusFound, "/admin/users"} return impart.HTTPError{http.StatusFound, "/admin/users"}
} }
userToToggle, err := app.db.GetUserForAuth(username) user, err := app.db.GetUserForAuth(username)
if err != nil { if err != nil {
log.Error("failed to get user: %v", err) log.Error("failed to get user: %v", err)
return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get user from username: %v", err)} return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get user from username: %v", err)}
} }
if userToToggle.Suspended { if user.Status == UserSuspended {
err = app.db.SetUserSuspended(userToToggle.ID, false) err = app.db.SetUserStatus(user.ID, UserActive)
} else { } else {
err = app.db.SetUserSuspended(userToToggle.ID, true) err = app.db.SetUserStatus(user.ID, UserSuspended)
} }
if err != nil { if err != nil {
log.Error("toggle user suspended: %v", err) log.Error("toggle user suspended: %v", err)
return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not toggle user suspended: %v")} return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not toggle user status: %v")}
} }
// TODO: invalidate sessions
return impart.HTTPError{http.StatusFound, fmt.Sprintf("/admin/user/%s#status", username)} return impart.HTTPError{http.StatusFound, fmt.Sprintf("/admin/user/%s#status", username)}
} }

View File

@ -71,6 +71,7 @@ type (
CurrentPage int CurrentPage int
TotalPages int TotalPages int
Format *CollectionFormat Format *CollectionFormat
Suspended bool
} }
SubmittedCollection struct { SubmittedCollection struct {
// Data used for updating a given collection // Data used for updating a given collection
@ -398,7 +399,7 @@ func newCollection(app *App, w http.ResponseWriter, r *http.Request) error {
} }
suspended, err := app.db.IsUserSuspended(userID) suspended, err := app.db.IsUserSuspended(userID)
if err != nil { if err != nil {
log.Error("new collection: get user: %v", err) log.Error("new collection: %v", err)
return ErrInternalGeneral return ErrInternalGeneral
} }
if suspended { if suspended {
@ -486,6 +487,7 @@ func fetchCollection(app *App, w http.ResponseWriter, r *http.Request) error {
res.Owner = u res.Owner = u
} }
} }
// TODO: check suspended
app.db.GetPostsCount(res, isCollOwner) app.db.GetPostsCount(res, isCollOwner)
// Strip non-public information // Strip non-public information
res.Collection.ForPublic() res.Collection.ForPublic()
@ -738,14 +740,10 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro
suspended, err := app.db.IsUserSuspended(c.OwnerID) suspended, err := app.db.IsUserSuspended(c.OwnerID)
if err != nil { if err != nil {
log.Error("view collection: get owner: %v", err) log.Error("view collection: %v", err)
return ErrInternalGeneral 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()
@ -802,6 +800,10 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro
log.Error("Error getting user for collection: %v", err) log.Error("Error getting user for collection: %v", err)
} }
} }
if !isOwner && suspended {
return ErrCollectionNotFound
}
displayPage.Suspended = isOwner && suspended
displayPage.Owner = owner displayPage.Owner = owner
coll.Owner = displayPage.Owner coll.Owner = displayPage.Owner
@ -853,10 +855,6 @@ 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)
@ -908,6 +906,10 @@ func handleViewCollectionTag(app *App, w http.ResponseWriter, r *http.Request) e
log.Error("Error getting user for collection: %v", err) log.Error("Error getting user for collection: %v", err)
} }
} }
if !isOwner && u.Status == UserSuspended {
return ErrCollectionNotFound
}
displayPage.Suspended = u.Status == UserSuspended
displayPage.Owner = owner displayPage.Owner = owner
coll.Owner = displayPage.Owner coll.Owner = displayPage.Owner
// Add more data // Add more data
@ -946,7 +948,7 @@ func existingCollection(app *App, w http.ResponseWriter, r *http.Request) error
collAlias := vars["alias"] collAlias := vars["alias"]
isWeb := r.FormValue("web") == "1" isWeb := r.FormValue("web") == "1"
var u *User u := &User{}
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")
@ -963,7 +965,7 @@ func existingCollection(app *App, w http.ResponseWriter, r *http.Request) error
suspended, err := app.db.IsUserSuspended(u.ID) suspended, err := app.db.IsUserSuspended(u.ID)
if err != nil { if err != nil {
log.Error("existing collection: get user suspended status: %v", err) log.Error("existing collection: %v", err)
return ErrInternalGeneral return ErrInternalGeneral
} }

View File

@ -296,7 +296,7 @@ func (db *datastore) CreateCollection(cfg *config.Config, alias, title string, u
func (db *datastore) GetUserByID(id int64) (*User, error) { func (db *datastore) GetUserByID(id int64) (*User, error) {
u := &User{ID: id} u := &User{ID: id}
err := db.QueryRow("SELECT username, password, email, created, suspended FROM users WHERE id = ?", id).Scan(&u.Username, &u.HashedPass, &u.Email, &u.Created, &u.Suspended) 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 { switch {
case err == sql.ErrNoRows: case err == sql.ErrNoRows:
return nil, ErrUserNotFound return nil, ErrUserNotFound
@ -313,16 +313,16 @@ func (db *datastore) GetUserByID(id int64) (*User, error) {
func (db *datastore) IsUserSuspended(id int64) (bool, error) { func (db *datastore) IsUserSuspended(id int64) (bool, error) {
u := &User{ID: id} u := &User{ID: id}
err := db.QueryRow("SELECT suspended FROM users WHERE id = ?", id).Scan(&u.Suspended) err := db.QueryRow("SELECT status FROM users WHERE id = ?", id).Scan(&u.Status)
switch { switch {
case err == sql.ErrNoRows: case err == sql.ErrNoRows:
return false, ErrUserNotFound return false, fmt.Errorf("is user suspended: %v", ErrUserNotFound)
case err != nil: case err != nil:
log.Error("Couldn't SELECT user password: %v", err) log.Error("Couldn't SELECT user password: %v", err)
return false, err return false, fmt.Errorf("is user suspended: %v", err)
} }
return u.Suspended, nil return u.Status == UserSuspended, 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
@ -364,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, suspended FROM users WHERE username = ?", username).Scan(&u.ID, &u.HashedPass, &u.Email, &u.Created, &u.Suspended) 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 { 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
@ -387,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, suspended FROM users WHERE id = ?", u.ID).Scan(&u.ID, &u.HashedPass, &u.Email, &u.Created, &u.Suspended) 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 { switch {
case err == sql.ErrNoRows: case err == sql.ErrNoRows:
return nil, ErrUserNotFound return nil, ErrUserNotFound
@ -1650,7 +1650,7 @@ func (db *datastore) GetTotalCollections() (collCount int64, err error) {
SELECT COUNT(*) SELECT COUNT(*)
FROM collections c FROM collections c
LEFT JOIN users u ON u.id = c.owner_id LEFT JOIN users u ON u.id = c.owner_id
WHERE u.suspended = 0`).Scan(&collCount) WHERE u.status = 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)
} }
@ -1662,7 +1662,7 @@ func (db *datastore) GetTotalPosts() (postCount int64, err error) {
SELECT COUNT(*) SELECT COUNT(*)
FROM posts p FROM posts p
LEFT JOIN users u ON u.id = p.owner_id LEFT JOIN users u ON u.id = p.owner_id
WHERE u.Suspended = 0`).Scan(&postCount) WHERE u.status = 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)
} }
@ -2384,7 +2384,7 @@ 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, suspended 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 { if err != nil {
log.Error("Failed selecting from users: %v", err) log.Error("Failed selecting from users: %v", err)
return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve all users."} return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve all users."}
@ -2394,7 +2394,7 @@ func (db *datastore) GetAllUsers(page uint) (*[]User, error) {
users := []User{} users := []User{}
for rows.Next() { for rows.Next() {
u := User{} u := User{}
err = rows.Scan(&u.ID, &u.Username, &u.Created, &u.Suspended) err = rows.Scan(&u.ID, &u.Username, &u.Created, &u.Status)
if err != nil { if err != nil {
log.Error("Failed scanning GetAllUsers() row: %v", err) log.Error("Failed scanning GetAllUsers() row: %v", err)
break break
@ -2431,10 +2431,11 @@ func (db *datastore) GetUserLastPostTime(id int64) (*time.Time, error) {
return &t, nil return &t, nil
} }
func (db *datastore) SetUserSuspended(id int64, suspend bool) error { // SetUserStatus changes a user's status in the database. see Users.UserStatus
_, err := db.Exec("UPDATE users SET suspended = ? WHERE id = ?", suspend, id) func (db *datastore) SetUserStatus(id int64, status UserStatus) error {
_, err := db.Exec("UPDATE users SET status = ? WHERE id = ?", status, id)
if err != nil { if err != nil {
return fmt.Errorf("failed to update user suspended status: %v", err) return fmt.Errorf("failed to update user status: %v", err)
} }
return nil return nil
} }

View File

@ -78,7 +78,7 @@ 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 { if u.Status == UserSuspended {
return ErrUserSuspended return ErrUserSuspended
} }

View File

@ -58,7 +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 () New("support users suspension", supportUserStatus), // V2 -> V3 (v0.11.0)
} }
// CurrentVer returns the current migration version the application is on // CurrentVer returns the current migration version the application is on

View File

@ -10,10 +10,10 @@
package migrations package migrations
func supportUserSuspension(db *datastore) error { func supportUserStatus(db *datastore) error {
t, err := db.Begin() t, err := db.Begin()
_, err = t.Exec(`ALTER TABLE users ADD COLUMN suspended ` + db.typeBool() + ` DEFAULT '0' NOT NULL`) _, err = t.Exec(`ALTER TABLE users ADD COLUMN status ` + db.typeInt() + ` DEFAULT '0' NOT NULL`)
if err != nil { if err != nil {
t.Rollback() t.Rollback()
return err return err

View File

@ -383,7 +383,7 @@ func handleViewPost(app *App, w http.ResponseWriter, r *http.Request) error {
suspended, err := app.db.IsUserSuspended(ownerID.Int64) suspended, err := app.db.IsUserSuspended(ownerID.Int64)
if err != nil { if err != nil {
log.Error("view post: get collection owner: %v", err) log.Error("view post: %v", err)
return ErrInternalGeneral return ErrInternalGeneral
} }
@ -509,7 +509,7 @@ func newPost(app *App, w http.ResponseWriter, r *http.Request) error {
} }
suspended, err := app.db.IsUserSuspended(userID) suspended, err := app.db.IsUserSuspended(userID)
if err != nil { if err != nil {
log.Error("new post: get user: %v", err) log.Error("new post: %v", err)
return ErrInternalGeneral return ErrInternalGeneral
} }
if suspended { if suspended {
@ -683,7 +683,7 @@ func existingPost(app *App, w http.ResponseWriter, r *http.Request) error {
suspended, err := app.db.IsUserSuspended(userID) suspended, err := app.db.IsUserSuspended(userID)
if err != nil { if err != nil {
log.Error("existing post: get user: %v", err) log.Error("existing post: %v", err)
return ErrInternalGeneral return ErrInternalGeneral
} }
if suspended { if suspended {
@ -886,7 +886,7 @@ func addPost(app *App, w http.ResponseWriter, r *http.Request) error {
suspended, err := app.db.IsUserSuspended(ownerID) suspended, err := app.db.IsUserSuspended(ownerID)
if err != nil { if err != nil {
log.Error("add post: get user: %v", err) log.Error("add post: %v", err)
return ErrInternalGeneral return ErrInternalGeneral
} }
if suspended { if suspended {
@ -989,7 +989,7 @@ func pinPost(app *App, w http.ResponseWriter, r *http.Request) error {
suspended, err := app.db.IsUserSuspended(userID) suspended, err := app.db.IsUserSuspended(userID)
if err != nil { if err != nil {
log.Error("pin post: get user: %v", err) log.Error("pin post: %v", err)
return ErrInternalGeneral return ErrInternalGeneral
} }
if suspended { if suspended {
@ -1063,7 +1063,7 @@ func fetchPost(app *App, w http.ResponseWriter, r *http.Request) error {
} }
suspended, err := app.db.IsUserSuspended(ownerID) suspended, err := app.db.IsUserSuspended(ownerID)
if err != nil { if err != nil {
log.Error("fetch post: get owner: %v", err) log.Error("fetch post: %v", err)
return ErrInternalGeneral return ErrInternalGeneral
} }
@ -1333,13 +1333,10 @@ func viewCollectionPost(app *App, w http.ResponseWriter, r *http.Request) error
suspended, err := app.db.IsUserSuspended(c.OwnerID) suspended, err := app.db.IsUserSuspended(c.OwnerID)
if err != nil { if err != nil {
log.Error("view collection post: get owner: %v", err) log.Error("view collection post: %v", err)
return ErrInternalGeneral 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
@ -1396,6 +1393,9 @@ Are you sure it was ever here?`,
p.Collection = coll p.Collection = coll
p.IsTopLevel = app.cfg.App.SingleUser p.IsTopLevel = app.cfg.App.SingleUser
if !p.IsOwner && suspended {
return ErrPostNotFound
}
// Check if post has been unpublished // Check if post has been unpublished
if p.Content == "" && p.Title.String == "" { if p.Content == "" && p.Title.String == "" {
return impart.HTTPError{http.StatusGone, "Post was unpublished."} return impart.HTTPError{http.StatusGone, "Post was unpublished."}
@ -1445,12 +1445,14 @@ Are you sure it was ever here?`,
IsFound bool IsFound bool
IsAdmin bool IsAdmin bool
CanInvite bool CanInvite bool
Suspended bool
}{ }{
PublicPost: p, PublicPost: p,
StaticPage: pageForReq(app, r), StaticPage: pageForReq(app, r),
IsOwner: cr.isCollOwner, IsOwner: cr.isCollOwner,
IsCustomDomain: cr.isCustomDomain, IsCustomDomain: cr.isCustomDomain,
IsFound: postFound, IsFound: postFound,
Suspended: suspended,
} }
tp.IsAdmin = u != nil && u.IsAdmin() tp.IsAdmin = u != nil && u.IsAdmin()
tp.CanInvite = canUserInvite(app.cfg, tp.IsAdmin) tp.CanInvite = canUserInvite(app.cfg, tp.IsAdmin)

View File

@ -71,7 +71,7 @@ func (app *App) FetchPublicPosts() (interface{}, error) {
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
LEFT JOIN users u ON u.id = p.owner_id 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 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`) 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)

View File

@ -144,7 +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/user/{username}/status", handler.Admin(handleAdminToggleUserStatus)).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")

View File

@ -225,7 +225,6 @@ 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;

View File

@ -214,8 +214,7 @@ 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
); );
-- -------------------------------------------------------- -- --------------------------------------------------------

View File

@ -11,10 +11,6 @@
package writefreely package writefreely
import ( 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" "html/template"
"io" "io"
"io/ioutil" "io/ioutil"
@ -22,6 +18,11 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/dustin/go-humanize"
"github.com/writeas/web-core/l10n"
"github.com/writeas/web-core/log"
"github.com/writeas/writefreely/config"
) )
var ( var (
@ -63,6 +64,7 @@ func initTemplate(parentDir, name string) {
filepath.Join(parentDir, templatesDir, name+".tmpl"), filepath.Join(parentDir, templatesDir, name+".tmpl"),
filepath.Join(parentDir, templatesDir, "include", "footer.tmpl"), filepath.Join(parentDir, templatesDir, "include", "footer.tmpl"),
filepath.Join(parentDir, templatesDir, "base.tmpl"), filepath.Join(parentDir, templatesDir, "base.tmpl"),
filepath.Join(parentDir, templatesDir, "user", "include", "suspended.tmpl"),
} }
if name == "collection" || name == "collection-tags" || name == "chorus-collection" { if name == "collection" || name == "collection-tags" || name == "chorus-collection" {
// These pages list out collection posts, so we also parse templatesDir + "include/posts.tmpl" // 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, path,
filepath.Join(parentDir, templatesDir, "include", "footer.tmpl"), filepath.Join(parentDir, templatesDir, "include", "footer.tmpl"),
filepath.Join(parentDir, templatesDir, "base.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, path,
filepath.Join(parentDir, templatesDir, "user", "include", "header.tmpl"), filepath.Join(parentDir, templatesDir, "user", "include", "header.tmpl"),
filepath.Join(parentDir, templatesDir, "user", "include", "footer.tmpl"), filepath.Join(parentDir, templatesDir, "user", "include", "footer.tmpl"),
filepath.Join(parentDir, templatesDir, "user", "include", "suspended.tmpl"),
)) ))
} }

View File

@ -65,6 +65,9 @@ article time.dt-published {
{{template "user-navigation" .}} {{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> <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 }} {{ if .Collection.ShowFooterBranding }}

View File

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

View File

@ -59,6 +59,9 @@
</nav> </nav>
</header> </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> <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 }} {{ if .Collection.ShowFooterBranding }}

View File

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

View File

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

View File

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

View File

@ -36,6 +36,9 @@
</head> </head>
<body id="post"> <body id="post">
{{if .Suspended}}
{{template "user-suspended"}}
{{end}}
<header> <header>
<h1 dir="{{.Direction}}"><a href="/">{{.SiteName}}</a></h1> <h1 dir="{{.Direction}}"><a href="/">{{.SiteName}}</a></h1>
<nav> <nav>

View File

@ -21,7 +21,7 @@
<td style="text-align:center"> <td style="text-align:center">
<a <a
href="/admin/user/{{.Username}}#status" href="/admin/user/{{.Username}}#status"
title="View or change account status">{{if .Suspended}}suspended{{else}}active{{end}}</a></td> title="View or change account status">{{if eq .Status 1}}suspended{{else}}active{{end}}</a></td>
</tr> </tr>
{{end}} {{end}}
</table> </table>

View File

@ -57,10 +57,10 @@ td.active-suspend > input[type="submit"] {
<td>{{if .LastPost}}{{.LastPost}}{{else}}Never{{end}}</td> <td>{{if .LastPost}}{{.LastPost}}{{else}}Never{{end}}</td>
</tr> </tr>
<tr> <tr>
<form action="/admin/user/{{.User.Username}}" method="POST"> <form action="/admin/user/{{.User.Username}}/status" method="POST">
<a id="status"/> <a id="status"/>
<th>Status</th> <th>Status</th>
{{if .User.Suspended}} {{if eq .User.Status 1}}
<td class="active-suspend"><p>User is currently Suspended</p><input type="submit" value="Activate"/></td> <td class="active-suspend"><p>User is currently Suspended</p><input type="submit" value="Activate"/></td>
{{else}} {{else}}
<td class="active-suspend"> <td class="active-suspend">

View File

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

View File

@ -8,6 +8,9 @@
<div class="content-container snug"> <div class="content-container snug">
<div id="overlay"></div> <div id="overlay"></div>
{{if .Suspended}}
{{template "user-suspended"}}
{{end}}
<h2>Customize {{.DisplayTitle}} <a href="{{if .SingleUser}}/{{else}}/{{.Alias}}/{{end}}">view blog</a></h2> <h2>Customize {{.DisplayTitle}} <a href="{{if .SingleUser}}/{{else}}/{{.Alias}}/{{end}}">view blog</a></h2>
{{if .Flashes}}<ul class="errors"> {{if .Flashes}}<ul class="errors">

View File

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

View File

@ -0,0 +1,6 @@
{{define "user-suspended"}}
<div class="alert info">
<p>This account is currently suspended.</p>
<p>Please contact the instance administrator to discuss reactivation.</p>
</div>
{{end}}

View File

@ -7,17 +7,14 @@ h3 { font-weight: normal; }
.section > *:not(input) { font-size: 0.86em; } .section > *:not(input) { font-size: 0.86em; }
</style> </style>
<div class="content-container snug regular"> <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> <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"> {{if .Flashes}}<ul class="errors">
{{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>

View File

@ -17,6 +17,9 @@ td.none {
</style> </style>
<div class="content-container snug"> <div class="content-container snug">
{{if .Suspended}}
{{template "user-suspended"}}
{{end}}
<h2 id="posts-header">{{if .Collection}}{{.Collection.DisplayTitle}} {{end}}Stats</h2> <h2 id="posts-header">{{if .Collection}}{{.Collection.DisplayTitle}} {{end}}Stats</h2>
<p>Stats for all time.</p> <p>Stats for all time.</p>

View File

@ -19,6 +19,13 @@ import (
"github.com/writeas/writefreely/key" "github.com/writeas/writefreely/key"
) )
type UserStatus int
const (
UserActive = iota
UserSuspended
)
type ( type (
userCredentials struct { userCredentials struct {
Alias string `json:"alias" schema:"alias"` Alias string `json:"alias" schema:"alias"`
@ -59,7 +66,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"` Status UserStatus `json:"status"`
clearEmail string `json:"email"` clearEmail string `json:"email"`
} }