From 77f7b4a522cf2dd522a64a86b32dee245f1cfd6f Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Wed, 28 Aug 2019 12:37:45 -0700 Subject: [PATCH 01/31] Add account suspension features This renders all requests for that user's posts, collections and related ActivityPub endpoints with 404 responses. While suspended, users may not create or edit posts or collections. User status is listed in the admin user page Admin view of user details shows status and now has a button to activate or suspend a user. --- account.go | 29 +++++++----- activitypub.go | 40 ++++++++++++++++ admin.go | 34 ++++++++++++-- collections.go | 34 +++++++++++++- database.go | 51 ++++++++++++++++---- errors.go | 5 +- feed.go | 14 +++++- invites.go | 13 +++-- migrations/migrations.go | 2 + migrations/v3.go | 29 ++++++++++++ pad.go | 22 +++++++-- posts.go | 73 +++++++++++++++++++++++++++-- read.go | 14 +++--- routes.go | 8 ++-- schema.sql | 1 + sqlite.sql | 3 +- templates/edit-meta.tmpl | 4 ++ templates/pad.tmpl | 6 ++- templates/user/admin/users.tmpl | 5 ++ templates/user/admin/view-user.tmpl | 32 +++++++++++++ templates/user/settings.tmpl | 6 +++ users.go | 1 + webfinger.go | 11 ++++- 23 files changed, 381 insertions(+), 56 deletions(-) create mode 100644 migrations/v3.go diff --git a/account.go b/account.go index 1cf259be..49700b45 100644 --- a/account.go +++ b/account.go @@ -13,6 +13,13 @@ package writefreely import ( "encoding/json" "fmt" + "html/template" + "net/http" + "regexp" + "strings" + "sync" + "time" + "github.com/gorilla/mux" "github.com/gorilla/sessions" "github.com/guregu/null/zero" @@ -22,12 +29,6 @@ import ( "github.com/writeas/web-core/log" "github.com/writeas/writefreely/author" "github.com/writeas/writefreely/page" - "html/template" - "net/http" - "regexp" - "strings" - "sync" - "time" ) type ( @@ -1011,14 +1012,16 @@ func viewSettings(app *App, u *User, w http.ResponseWriter, r *http.Request) err obj := struct { *UserPage - Email string - HasPass bool - IsLogOut bool + Email string + HasPass bool + IsLogOut bool + Suspended bool }{ - UserPage: NewUserPage(app, r, u, "Account Settings", flashes), - Email: fullUser.EmailClear(app.keys), - HasPass: passIsSet, - IsLogOut: r.FormValue("logout") == "1", + UserPage: NewUserPage(app, r, u, "Account Settings", flashes), + Email: fullUser.EmailClear(app.keys), + HasPass: passIsSet, + IsLogOut: r.FormValue("logout") == "1", + Suspended: fullUser.Suspended, } showUserPage(w, "settings", obj) diff --git a/activitypub.go b/activitypub.go index 997609d6..c10838dc 100644 --- a/activitypub.go +++ b/activitypub.go @@ -80,6 +80,14 @@ func handleFetchCollectionActivities(app *App, w http.ResponseWriter, r *http.Re if err != nil { return err } + suspended, err := app.db.IsUserSuspended(c.OwnerID) + if err != nil { + log.Error("fetch collection inbox: get owner: %v", err) + return ErrInternalGeneral + } + if suspended { + return ErrCollectionNotFound + } c.hostName = app.cfg.App.Host p := c.PersonObject() @@ -105,6 +113,14 @@ func handleFetchCollectionOutbox(app *App, w http.ResponseWriter, r *http.Reques if err != nil { return err } + suspended, err := app.db.IsUserSuspended(c.OwnerID) + if err != nil { + log.Error("fetch collection inbox: get owner: %v", err) + return ErrInternalGeneral + } + if suspended { + return ErrCollectionNotFound + } c.hostName = app.cfg.App.Host if app.cfg.App.SingleUser { @@ -158,6 +174,14 @@ func handleFetchCollectionFollowers(app *App, w http.ResponseWriter, r *http.Req if err != nil { return err } + suspended, err := app.db.IsUserSuspended(c.OwnerID) + if err != nil { + log.Error("fetch collection inbox: get owner: %v", err) + return ErrInternalGeneral + } + if suspended { + return ErrCollectionNotFound + } c.hostName = app.cfg.App.Host accountRoot := c.FederatedAccount() @@ -204,6 +228,14 @@ func handleFetchCollectionFollowing(app *App, w http.ResponseWriter, r *http.Req if err != nil { return err } + suspended, err := app.db.IsUserSuspended(c.OwnerID) + if err != nil { + log.Error("fetch collection inbox: get owner: %v", err) + return ErrInternalGeneral + } + if suspended { + return ErrCollectionNotFound + } c.hostName = app.cfg.App.Host accountRoot := c.FederatedAccount() @@ -238,6 +270,14 @@ func handleFetchCollectionInbox(app *App, w http.ResponseWriter, r *http.Request // TODO: return Reject? return err } + suspended, err := app.db.IsUserSuspended(c.OwnerID) + if err != nil { + log.Error("fetch collection inbox: get owner: %v", err) + return ErrInternalGeneral + } + if suspended { + return ErrCollectionNotFound + } c.hostName = app.cfg.App.Host if debugging { diff --git a/admin.go b/admin.go index fe19ad5a..dc8580d2 100644 --- a/admin.go +++ b/admin.go @@ -13,16 +13,17 @@ package writefreely import ( "database/sql" "fmt" + "net/http" + "runtime" + "strconv" + "time" + "github.com/gogits/gogs/pkg/tool" "github.com/gorilla/mux" "github.com/writeas/impart" "github.com/writeas/web-core/auth" "github.com/writeas/web-core/log" "github.com/writeas/writefreely/config" - "net/http" - "runtime" - "strconv" - "time" ) var ( @@ -229,6 +230,31 @@ func handleViewAdminUser(app *App, u *User, w http.ResponseWriter, r *http.Reque return nil } +func handleAdminToggleUserSuspended(app *App, u *User, w http.ResponseWriter, r *http.Request) error { + vars := mux.Vars(r) + username := vars["username"] + if username == "" { + return impart.HTTPError{http.StatusFound, "/admin/users"} + } + + userToToggle, err := app.db.GetUserForAuth(username) + if err != nil { + log.Error("failed to get user: %v", err) + return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get user from username: %v", err)} + } + if userToToggle.Suspended { + err = app.db.SetUserSuspended(userToToggle.ID, false) + } else { + err = app.db.SetUserSuspended(userToToggle.ID, true) + } + if err != nil { + log.Error("toggle user suspended: %v", err) + return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not toggle user suspended: %v")} + } + // TODO: invalidate sessions + return impart.HTTPError{http.StatusFound, fmt.Sprintf("/admin/user/%s#status", username)} +} + func handleViewAdminPages(app *App, u *User, w http.ResponseWriter, r *http.Request) error { p := struct { *UserPage diff --git a/collections.go b/collections.go index aee74f7f..95dd8086 100644 --- a/collections.go +++ b/collections.go @@ -379,6 +379,7 @@ func newCollection(app *App, w http.ResponseWriter, r *http.Request) error { } var userID int64 + var err error if reqJSON && !c.Web { accessToken = r.Header.Get("Authorization") if accessToken == "" { @@ -395,6 +396,14 @@ func newCollection(app *App, w http.ResponseWriter, r *http.Request) error { } userID = u.ID } + suspended, err := app.db.IsUserSuspended(userID) + if err != nil { + log.Error("new collection: get user: %v", err) + return ErrInternalGeneral + } + if suspended { + return ErrUserSuspended + } if !author.IsValidUsername(app.cfg, c.Alias) { return impart.HTTPError{http.StatusPreconditionFailed, "Collection alias isn't valid."} @@ -724,6 +733,15 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro return err } + suspended, err := app.db.IsUserSuspended(c.OwnerID) + if err != nil { + log.Error("view collection: get owner: %v", err) + return ErrInternalGeneral + } + + if suspended { + return ErrCollectionNotFound + } // Serve ActivityStreams data now, if requested if strings.Contains(r.Header.Get("Accept"), "application/activity+json") { ac := c.PersonObject() @@ -824,6 +842,10 @@ func handleViewCollectionTag(app *App, w http.ResponseWriter, r *http.Request) e return err } + if u.Suspended { + return ErrCollectionNotFound + } + page := getCollectionPage(vars) c, err := processCollectionPermissions(app, cr, u, w, r) @@ -916,7 +938,6 @@ func existingCollection(app *App, w http.ResponseWriter, r *http.Request) error if reqJSON && !isWeb { // Ensure an access token was given accessToken := r.Header.Get("Authorization") - u = &User{} u.ID = app.db.GetUserID(accessToken) if u.ID == -1 { return ErrBadAccessToken @@ -928,6 +949,16 @@ func existingCollection(app *App, w http.ResponseWriter, r *http.Request) error } } + suspended, err := app.db.IsUserSuspended(u.ID) + if err != nil { + log.Error("existing collection: get user suspended status: %v", err) + return ErrInternalGeneral + } + + if suspended { + return ErrUserSuspended + } + if r.Method == "DELETE" { err := app.db.DeleteCollection(collAlias, u.ID) if err != nil { @@ -940,7 +971,6 @@ func existingCollection(app *App, w http.ResponseWriter, r *http.Request) error } c := SubmittedCollection{OwnerID: uint64(u.ID)} - var err error if reqJSON { // Decode JSON request diff --git a/database.go b/database.go index 34c5234d..150a74f9 100644 --- a/database.go +++ b/database.go @@ -296,7 +296,7 @@ func (db *datastore) CreateCollection(cfg *config.Config, alias, title string, u func (db *datastore) GetUserByID(id int64) (*User, error) { u := &User{ID: id} - err := db.QueryRow("SELECT username, password, email, created FROM users WHERE id = ?", id).Scan(&u.Username, &u.HashedPass, &u.Email, &u.Created) + err := db.QueryRow("SELECT username, password, email, created, suspended FROM users WHERE id = ?", id).Scan(&u.Username, &u.HashedPass, &u.Email, &u.Created, &u.Suspended) switch { case err == sql.ErrNoRows: return nil, ErrUserNotFound @@ -308,6 +308,23 @@ func (db *datastore) GetUserByID(id int64) (*User, error) { return u, nil } +// IsUserSuspended returns true if the user account associated with id is +// currently suspended. +func (db *datastore) IsUserSuspended(id int64) (bool, error) { + u := &User{ID: id} + + err := db.QueryRow("SELECT suspended FROM users WHERE id = ?", id).Scan(&u.Suspended) + switch { + case err == sql.ErrNoRows: + return false, ErrUserNotFound + case err != nil: + log.Error("Couldn't SELECT user password: %v", err) + return false, err + } + + return u.Suspended, nil +} + // DoesUserNeedAuth returns true if the user hasn't provided any methods for // authenticating with the account, such a passphrase or email address. // Any errors are reported to admin and silently quashed, returning false as the @@ -347,7 +364,7 @@ func (db *datastore) IsUserPassSet(id int64) (bool, error) { func (db *datastore) GetUserForAuth(username string) (*User, error) { u := &User{Username: username} - err := db.QueryRow("SELECT id, password, email, created FROM users WHERE username = ?", username).Scan(&u.ID, &u.HashedPass, &u.Email, &u.Created) + err := db.QueryRow("SELECT id, password, email, created, suspended FROM users WHERE username = ?", username).Scan(&u.ID, &u.HashedPass, &u.Email, &u.Created, &u.Suspended) switch { case err == sql.ErrNoRows: // Check if they've entered the wrong, unnormalized username @@ -370,7 +387,7 @@ func (db *datastore) GetUserForAuth(username string) (*User, error) { func (db *datastore) GetUserForAuthByID(userID int64) (*User, error) { u := &User{ID: userID} - err := db.QueryRow("SELECT id, password, email, created FROM users WHERE id = ?", u.ID).Scan(&u.ID, &u.HashedPass, &u.Email, &u.Created) + err := db.QueryRow("SELECT id, password, email, created, suspended FROM users WHERE id = ?", u.ID).Scan(&u.ID, &u.HashedPass, &u.Email, &u.Created, &u.Suspended) switch { case err == sql.ErrNoRows: return nil, ErrUserNotFound @@ -1624,7 +1641,11 @@ func (db *datastore) GetMeStats(u *User) userMeStats { } func (db *datastore) GetTotalCollections() (collCount int64, err error) { - err = db.QueryRow(`SELECT COUNT(*) FROM collections`).Scan(&collCount) + err = db.QueryRow(` + SELECT COUNT(*) + FROM collections c + LEFT JOIN users u ON u.id = c.owner_id + WHERE u.suspended = 0`).Scan(&collCount) if err != nil { log.Error("Unable to fetch collections count: %v", err) } @@ -1632,7 +1653,11 @@ func (db *datastore) GetTotalCollections() (collCount int64, err error) { } func (db *datastore) GetTotalPosts() (postCount int64, err error) { - err = db.QueryRow(`SELECT COUNT(*) FROM posts`).Scan(&postCount) + err = db.QueryRow(` + SELECT COUNT(*) + FROM posts p + LEFT JOIN users u ON u.id = p.owner_id + WHERE u.Suspended = 0`).Scan(&postCount) if err != nil { log.Error("Unable to fetch posts count: %v", err) } @@ -2341,17 +2366,17 @@ func (db *datastore) GetAllUsers(page uint) (*[]User, error) { limitStr = fmt.Sprintf("%d, %d", (page-1)*adminUsersPerPage, adminUsersPerPage) } - rows, err := db.Query("SELECT id, username, created FROM users ORDER BY created DESC LIMIT " + limitStr) + rows, err := db.Query("SELECT id, username, created, suspended FROM users ORDER BY created DESC LIMIT " + limitStr) if err != nil { - log.Error("Failed selecting from posts: %v", err) - return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve user posts."} + log.Error("Failed selecting from users: %v", err) + return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve all users."} } defer rows.Close() users := []User{} for rows.Next() { u := User{} - err = rows.Scan(&u.ID, &u.Username, &u.Created) + err = rows.Scan(&u.ID, &u.Username, &u.Created, &u.Suspended) if err != nil { log.Error("Failed scanning GetAllUsers() row: %v", err) break @@ -2388,6 +2413,14 @@ func (db *datastore) GetUserLastPostTime(id int64) (*time.Time, error) { return &t, nil } +func (db *datastore) SetUserSuspended(id int64, suspend bool) error { + _, err := db.Exec("UPDATE users SET suspended = ? WHERE id = ?", suspend, id) + if err != nil { + return fmt.Errorf("failed to update user suspended status: %v", err) + } + return nil +} + func (db *datastore) GetCollectionLastPostTime(id int64) (*time.Time, error) { var t time.Time err := db.QueryRow("SELECT created FROM posts WHERE collection_id = ? ORDER BY created DESC LIMIT 1", id).Scan(&t) diff --git a/errors.go b/errors.go index 0092b7fe..fa7304f2 100644 --- a/errors.go +++ b/errors.go @@ -11,8 +11,9 @@ package writefreely import ( - "github.com/writeas/impart" "net/http" + + "github.com/writeas/impart" ) // Commonly returned HTTP errors @@ -46,6 +47,8 @@ var ( ErrUserNotFound = impart.HTTPError{http.StatusNotFound, "User doesn't exist."} ErrUserNotFoundEmail = impart.HTTPError{http.StatusNotFound, "Please enter your username instead of your email address."} + + ErrUserSuspended = impart.HTTPError{http.StatusForbidden, "Account is suspended, contact the administrator."} ) // Post operation errors diff --git a/feed.go b/feed.go index dd82c33f..353b1b96 100644 --- a/feed.go +++ b/feed.go @@ -12,12 +12,13 @@ package writefreely import ( "fmt" + "net/http" + "time" + . "github.com/gorilla/feeds" "github.com/gorilla/mux" stripmd "github.com/writeas/go-strip-markdown" "github.com/writeas/web-core/log" - "net/http" - "time" ) func ViewFeed(app *App, w http.ResponseWriter, req *http.Request) error { @@ -34,6 +35,15 @@ func ViewFeed(app *App, w http.ResponseWriter, req *http.Request) error { if err != nil { return nil } + + suspended, err := app.db.IsUserSuspended(c.OwnerID) + if err != nil { + log.Error("view feed: get user: %v", err) + return ErrInternalGeneral + } + if suspended { + return ErrCollectionNotFound + } c.hostName = app.cfg.App.Host if c.IsPrivate() || c.IsProtected() { diff --git a/invites.go b/invites.go index 561255f0..93b82b49 100644 --- a/invites.go +++ b/invites.go @@ -12,15 +12,16 @@ package writefreely import ( "database/sql" + "html/template" + "net/http" + "strconv" + "time" + "github.com/gorilla/mux" "github.com/writeas/impart" "github.com/writeas/nerds/store" "github.com/writeas/web-core/log" "github.com/writeas/writefreely/page" - "html/template" - "net/http" - "strconv" - "time" ) type Invite struct { @@ -77,6 +78,10 @@ func handleCreateUserInvite(app *App, u *User, w http.ResponseWriter, r *http.Re muVal := r.FormValue("uses") expVal := r.FormValue("expires") + if u.Suspended { + return ErrUserSuspended + } + var err error var maxUses int if muVal != "0" { diff --git a/migrations/migrations.go b/migrations/migrations.go index 70e4b7b0..de3f4874 100644 --- a/migrations/migrations.go +++ b/migrations/migrations.go @@ -13,6 +13,7 @@ package migrations import ( "database/sql" + "github.com/writeas/web-core/log" ) @@ -57,6 +58,7 @@ func (m *migration) Migrate(db *datastore) error { var migrations = []Migration{ New("support user invites", supportUserInvites), // -> V1 (v0.8.0) New("support dynamic instance pages", supportInstancePages), // V1 -> V2 (v0.9.0) + New("support users suspension", supportUserSuspension), // V2 -> V3 () } // CurrentVer returns the current migration version the application is on diff --git a/migrations/v3.go b/migrations/v3.go new file mode 100644 index 00000000..c7c00a92 --- /dev/null +++ b/migrations/v3.go @@ -0,0 +1,29 @@ +/* + * Copyright © 2019 A Bunch Tell LLC. + * + * This file is part of WriteFreely. + * + * WriteFreely is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, included + * in the LICENSE file in this source code package. + */ + +package migrations + +func supportUserSuspension(db *datastore) error { + t, err := db.Begin() + + _, err = t.Exec(`ALTER TABLE users ADD COLUMN suspended ` + db.typeBool() + ` DEFAULT '0' NOT NULL`) + if err != nil { + t.Rollback() + return err + } + + err = t.Commit() + if err != nil { + t.Rollback() + return err + } + + return nil +} diff --git a/pad.go b/pad.go index 1545b4f8..8a54b76f 100644 --- a/pad.go +++ b/pad.go @@ -11,12 +11,13 @@ package writefreely import ( + "net/http" + "strings" + "github.com/gorilla/mux" "github.com/writeas/impart" "github.com/writeas/web-core/log" "github.com/writeas/writefreely/page" - "net/http" - "strings" ) func handleViewPad(app *App, w http.ResponseWriter, r *http.Request) error { @@ -34,9 +35,10 @@ func handleViewPad(app *App, w http.ResponseWriter, r *http.Request) error { } appData := &struct { page.StaticPage - Post *RawPost - User *User - Blogs *[]Collection + Post *RawPost + User *User + Blogs *[]Collection + Suspended bool Editing bool // True if we're modifying an existing post EditCollection *Collection // Collection of the post we're editing, if any @@ -51,6 +53,10 @@ func handleViewPad(app *App, w http.ResponseWriter, r *http.Request) error { if err != nil { log.Error("Unable to get user's blogs for Pad: %v", err) } + appData.Suspended, err = app.db.IsUserSuspended(appData.User.ID) + if err != nil { + log.Error("Unable to get users suspension status for Pad: %v", err) + } } padTmpl := app.cfg.App.Editor @@ -121,12 +127,18 @@ func handleViewMeta(app *App, w http.ResponseWriter, r *http.Request) error { EditCollection *Collection // Collection of the post we're editing, if any Flashes []string NeedsToken bool + Suspended bool }{ StaticPage: pageForReq(app, r), Post: &RawPost{Font: "norm"}, User: getUserSession(app, r), } var err error + appData.Suspended, err = app.db.IsUserSuspended(appData.User.ID) + if err != nil { + log.Error("view meta: get user suspended status: %v", err) + return ErrInternalGeneral + } if action == "" && slug == "" { return ErrPostNotFound diff --git a/posts.go b/posts.go index 2f3606f1..2d648083 100644 --- a/posts.go +++ b/posts.go @@ -380,6 +380,16 @@ func handleViewPost(app *App, w http.ResponseWriter, r *http.Request) error { } } + suspended, err := app.db.IsUserSuspended(ownerID.Int64) + if err != nil { + log.Error("view post: get collection owner: %v", err) + return ErrInternalGeneral + } + + if suspended { + return ErrPostNotFound + } + // Check if post has been unpublished if content == "" { gone = true @@ -496,6 +506,15 @@ func newPost(app *App, w http.ResponseWriter, r *http.Request) error { } else { userID = app.db.GetUserID(accessToken) } + suspended, err := app.db.IsUserSuspended(userID) + if err != nil { + log.Error("new post: get user: %v", err) + return ErrInternalGeneral + } + if suspended { + return ErrUserSuspended + } + if userID == -1 { return ErrNotLoggedIn } @@ -508,7 +527,7 @@ func newPost(app *App, w http.ResponseWriter, r *http.Request) error { var p *SubmittedPost if reqJSON { decoder := json.NewDecoder(r.Body) - err := decoder.Decode(&p) + err = decoder.Decode(&p) if err != nil { log.Error("Couldn't parse new post JSON request: %v\n", err) return ErrBadJSON @@ -554,7 +573,6 @@ func newPost(app *App, w http.ResponseWriter, r *http.Request) error { var newPost *PublicPost = &PublicPost{} var coll *Collection - var err error if accessToken != "" { newPost, err = app.db.CreateOwnedPost(p, accessToken, collAlias, app.cfg.App.Host) } else { @@ -662,6 +680,15 @@ func existingPost(app *App, w http.ResponseWriter, r *http.Request) error { } } + suspended, err := app.db.IsUserSuspended(userID) + if err != nil { + log.Error("existing post: get user: %v", err) + return ErrInternalGeneral + } + if suspended { + return ErrUserSuspended + } + // Modify post struct p.ID = postID @@ -856,11 +883,20 @@ func addPost(app *App, w http.ResponseWriter, r *http.Request) error { ownerID = u.ID } + suspended, err := app.db.IsUserSuspended(ownerID) + if err != nil { + log.Error("add post: get user: %v", err) + return ErrInternalGeneral + } + if suspended { + return ErrUserSuspended + } + // Parse claimed posts in format: // [{"id": "...", "token": "..."}] var claims *[]ClaimPostRequest decoder := json.NewDecoder(r.Body) - err := decoder.Decode(&claims) + err = decoder.Decode(&claims) if err != nil { return ErrBadJSONArray } @@ -950,13 +986,22 @@ func pinPost(app *App, w http.ResponseWriter, r *http.Request) error { userID = u.ID } + suspended, err := app.db.IsUserSuspended(userID) + if err != nil { + log.Error("pin post: get user: %v", err) + return ErrInternalGeneral + } + if suspended { + return ErrUserSuspended + } + // Parse request var posts []struct { ID string `json:"id"` Position int64 `json:"position"` } decoder := json.NewDecoder(r.Body) - err := decoder.Decode(&posts) + err = decoder.Decode(&posts) if err != nil { return ErrBadJSONArray } @@ -992,6 +1037,7 @@ func pinPost(app *App, w http.ResponseWriter, r *http.Request) error { func fetchPost(app *App, w http.ResponseWriter, r *http.Request) error { var collID int64 + var ownerID int64 var coll *Collection var err error vars := mux.Vars(r) @@ -1007,12 +1053,22 @@ func fetchPost(app *App, w http.ResponseWriter, r *http.Request) error { return err } collID = coll.ID + ownerID = coll.OwnerID } p, err := app.db.GetPost(vars["post"], collID) if err != nil { return err } + suspended, err := app.db.IsUserSuspended(ownerID) + if err != nil { + log.Error("fetch post: get owner: %v", err) + return ErrInternalGeneral + } + + if suspended { + return ErrPostNotFound + } p.extractData() @@ -1270,6 +1326,15 @@ func viewCollectionPost(app *App, w http.ResponseWriter, r *http.Request) error } c.hostName = app.cfg.App.Host + suspended, err := app.db.IsUserSuspended(c.OwnerID) + if err != nil { + log.Error("view collection post: get owner: %v", err) + return ErrInternalGeneral + } + + if suspended { + return ErrPostNotFound + } // Check collection permissions if c.IsPrivate() && (u == nil || u.ID != c.OwnerID) { return ErrPostNotFound diff --git a/read.go b/read.go index 3bc91c71..e7d1e55d 100644 --- a/read.go +++ b/read.go @@ -13,6 +13,12 @@ package writefreely import ( "database/sql" "fmt" + "html/template" + "math" + "net/http" + "strconv" + "time" + . "github.com/gorilla/feeds" "github.com/gorilla/mux" stripmd "github.com/writeas/go-strip-markdown" @@ -20,11 +26,6 @@ import ( "github.com/writeas/web-core/log" "github.com/writeas/web-core/memo" "github.com/writeas/writefreely/page" - "html/template" - "math" - "net/http" - "strconv" - "time" ) const ( @@ -62,7 +63,8 @@ func (app *App) FetchPublicPosts() (interface{}, error) { rows, err := app.db.Query(`SELECT p.id, alias, c.title, p.slug, p.title, p.content, p.text_appearance, p.language, p.rtl, p.created, p.updated FROM collections c LEFT JOIN posts p ON p.collection_id = c.id - WHERE c.privacy = 1 AND (p.created >= ` + app.db.dateSub(3, "month") + ` AND p.created <= ` + app.db.now() + ` AND pinned_position IS NULL) + LEFT JOIN users u ON u.id = p.owner_id + WHERE c.privacy = 1 AND (p.created >= ` + app.db.dateSub(3, "month") + ` AND p.created <= ` + app.db.now() + ` AND pinned_position IS NULL) AND u.suspended = 0 ORDER BY p.created DESC`) if err != nil { log.Error("Failed selecting from posts: %v", err) diff --git a/routes.go b/routes.go index 724c5320..e7014cd5 100644 --- a/routes.go +++ b/routes.go @@ -11,13 +11,14 @@ package writefreely import ( + "net/http" + "path/filepath" + "strings" + "github.com/gorilla/mux" "github.com/writeas/go-webfinger" "github.com/writeas/web-core/log" "github.com/writefreely/go-nodeinfo" - "net/http" - "path/filepath" - "strings" ) // InitStaticRoutes adds routes for serving static files. @@ -143,6 +144,7 @@ func InitRoutes(apper Apper, r *mux.Router) *mux.Router { write.HandleFunc("/admin", handler.Admin(handleViewAdminDash)).Methods("GET") write.HandleFunc("/admin/users", handler.Admin(handleViewAdminUsers)).Methods("GET") write.HandleFunc("/admin/user/{username}", handler.Admin(handleViewAdminUser)).Methods("GET") + write.HandleFunc("/admin/user/{username}", handler.Admin(handleAdminToggleUserSuspended)).Methods("POST") write.HandleFunc("/admin/pages", handler.Admin(handleViewAdminPages)).Methods("GET") write.HandleFunc("/admin/page/{slug}", handler.Admin(handleViewAdminPage)).Methods("GET") write.HandleFunc("/admin/update/config", handler.AdminApper(handleAdminUpdateConfig)).Methods("POST") diff --git a/schema.sql b/schema.sql index b3fae97a..3a797363 100644 --- a/schema.sql +++ b/schema.sql @@ -225,6 +225,7 @@ CREATE TABLE IF NOT EXISTS `users` ( `password` char(60) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, `email` varbinary(255) DEFAULT NULL, `created` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + `suspended` tinyint(1) NOT NULL DEFAULT 0, PRIMARY KEY (`id`), UNIQUE KEY `username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1; diff --git a/sqlite.sql b/sqlite.sql index 90989ed5..920ffeda 100644 --- a/sqlite.sql +++ b/sqlite.sql @@ -214,7 +214,8 @@ CREATE TABLE IF NOT EXISTS `users` ( username TEXT NOT NULL UNIQUE, password TEXT NOT NULL, email TEXT DEFAULT NULL, - created DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP + created DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + suspended INTEGER NOT NULL DEFAULT 0 ); -- -------------------------------------------------------- diff --git a/templates/edit-meta.tmpl b/templates/edit-meta.tmpl index 8d96b15e..6707e68e 100644 --- a/templates/edit-meta.tmpl +++ b/templates/edit-meta.tmpl @@ -269,6 +269,10 @@ {{template "footer" .}} {{end}} From f85f0751a3a7480bc29450cf42a4360c31ff3083 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Fri, 25 Oct 2019 12:04:24 -0700 Subject: [PATCH 03/31] 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 --- account.go | 29 ++++++++++++++++++++++++++- activitypub.go | 10 ++++----- admin.go | 13 ++++++------ collections.go | 26 +++++++++++++----------- database.go | 29 ++++++++++++++------------- invites.go | 2 +- migrations/migrations.go | 2 +- migrations/v3.go | 4 ++-- posts.go | 22 +++++++++++--------- read.go | 2 +- routes.go | 2 +- schema.sql | 1 - sqlite.sql | 3 +-- templates.go | 12 +++++++---- templates/chorus-collection-post.tmpl | 3 +++ templates/chorus-collection.tmpl | 3 +++ templates/collection-post.tmpl | 3 +++ templates/collection-tags.tmpl | 3 +++ templates/collection.tmpl | 3 +++ templates/password-collection.tmpl | 3 +++ templates/post.tmpl | 3 +++ templates/user/admin/users.tmpl | 2 +- templates/user/admin/view-user.tmpl | 4 ++-- templates/user/articles.tmpl | 3 +++ templates/user/collection.tmpl | 3 +++ templates/user/collections.tmpl | 3 +++ templates/user/include/suspended.tmpl | 6 ++++++ templates/user/settings.tmpl | 9 +++------ templates/user/stats.tmpl | 3 +++ users.go | 9 ++++++++- 30 files changed, 148 insertions(+), 72 deletions(-) create mode 100644 templates/user/include/suspended.tmpl diff --git a/account.go b/account.go index c3d55ba7..0faa7bbc 100644 --- a/account.go +++ b/account.go @@ -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) @@ -1026,7 +1053,7 @@ func viewSettings(app *App, u *User, w http.ResponseWriter, r *http.Request) err Email: fullUser.EmailClear(app.keys), HasPass: passIsSet, IsLogOut: r.FormValue("logout") == "1", - Suspended: fullUser.Suspended, + Suspended: fullUser.Status == UserSuspended, } showUserPage(w, "settings", obj) diff --git a/activitypub.go b/activitypub.go index 06b04688..eeaa1fa6 100644 --- a/activitypub.go +++ b/activitypub.go @@ -82,7 +82,7 @@ func handleFetchCollectionActivities(app *App, w http.ResponseWriter, r *http.Re } suspended, err := app.db.IsUserSuspended(c.OwnerID) if err != nil { - log.Error("fetch collection inbox: get owner: %v", err) + log.Error("fetch collection activities: %v", err) return ErrInternalGeneral } if suspended { @@ -115,7 +115,7 @@ func handleFetchCollectionOutbox(app *App, w http.ResponseWriter, r *http.Reques } suspended, err := app.db.IsUserSuspended(c.OwnerID) if err != nil { - log.Error("fetch collection inbox: get owner: %v", err) + log.Error("fetch collection outbox: %v", err) return ErrInternalGeneral } if suspended { @@ -176,7 +176,7 @@ func handleFetchCollectionFollowers(app *App, w http.ResponseWriter, r *http.Req } suspended, err := app.db.IsUserSuspended(c.OwnerID) if err != nil { - log.Error("fetch collection inbox: get owner: %v", err) + log.Error("fetch collection followers: %v", err) return ErrInternalGeneral } if suspended { @@ -230,7 +230,7 @@ func handleFetchCollectionFollowing(app *App, w http.ResponseWriter, r *http.Req } suspended, err := app.db.IsUserSuspended(c.OwnerID) if err != nil { - log.Error("fetch collection inbox: get owner: %v", err) + log.Error("fetch collection following: %v", err) return ErrInternalGeneral } if suspended { @@ -272,7 +272,7 @@ func handleFetchCollectionInbox(app *App, w http.ResponseWriter, r *http.Request } suspended, err := app.db.IsUserSuspended(c.OwnerID) if err != nil { - log.Error("fetch collection inbox: get owner: %v", err) + log.Error("fetch collection inbox: %v", err) return ErrInternalGeneral } if suspended { diff --git a/admin.go b/admin.go index 1d949871..65afd5f7 100644 --- a/admin.go +++ b/admin.go @@ -230,28 +230,27 @@ func handleViewAdminUser(app *App, u *User, w http.ResponseWriter, r *http.Reque return nil } -func handleAdminToggleUserSuspended(app *App, u *User, w http.ResponseWriter, r *http.Request) error { +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"} } - userToToggle, err := app.db.GetUserForAuth(username) + 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 userToToggle.Suspended { - err = app.db.SetUserSuspended(userToToggle.ID, false) + if user.Status == UserSuspended { + err = app.db.SetUserStatus(user.ID, UserActive) } else { - err = app.db.SetUserSuspended(userToToggle.ID, true) + err = app.db.SetUserStatus(user.ID, UserSuspended) } if err != nil { 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)} } diff --git a/collections.go b/collections.go index c4cefeb6..38ec6f10 100644 --- a/collections.go +++ b/collections.go @@ -71,6 +71,7 @@ type ( CurrentPage int TotalPages int Format *CollectionFormat + Suspended bool } SubmittedCollection struct { // 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) if err != nil { - log.Error("new collection: get user: %v", err) + log.Error("new collection: %v", err) return ErrInternalGeneral } if suspended { @@ -486,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() @@ -738,14 +740,10 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro suspended, err := app.db.IsUserSuspended(c.OwnerID) if err != nil { - log.Error("view collection: get owner: %v", err) + log.Error("view collection: %v", err) return ErrInternalGeneral } - if suspended { - return ErrCollectionNotFound - } - // Serve ActivityStreams data now, if requested if strings.Contains(r.Header.Get("Accept"), "application/activity+json") { ac := c.PersonObject() @@ -802,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 @@ -853,10 +855,6 @@ func handleViewCollectionTag(app *App, w http.ResponseWriter, r *http.Request) e return err } - if u.Suspended { - return ErrCollectionNotFound - } - page := getCollectionPage(vars) c, err := processCollectionPermissions(app, cr, u, w, r) @@ -908,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.Status == UserSuspended { + return ErrCollectionNotFound + } + displayPage.Suspended = u.Status == UserSuspended displayPage.Owner = owner coll.Owner = displayPage.Owner // Add more data @@ -946,7 +948,7 @@ 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") @@ -963,7 +965,7 @@ 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) + log.Error("existing collection: %v", err) return ErrInternalGeneral } diff --git a/database.go b/database.go index b465e680..4b0c702b 100644 --- a/database.go +++ b/database.go @@ -296,7 +296,7 @@ func (db *datastore) CreateCollection(cfg *config.Config, alias, title string, u func (db *datastore) GetUserByID(id int64) (*User, error) { u := &User{ID: id} - err := db.QueryRow("SELECT username, password, email, created, 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 { case err == sql.ErrNoRows: return nil, ErrUserNotFound @@ -313,16 +313,16 @@ func (db *datastore) GetUserByID(id int64) (*User, error) { 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) + err := db.QueryRow("SELECT status FROM users WHERE id = ?", id).Scan(&u.Status) switch { case err == sql.ErrNoRows: - return false, ErrUserNotFound + return false, fmt.Errorf("is user suspended: %v", ErrUserNotFound) case err != nil: 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 @@ -364,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, 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 { case err == sql.ErrNoRows: // 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) { 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 { case err == sql.ErrNoRows: return nil, ErrUserNotFound @@ -1650,7 +1650,7 @@ func (db *datastore) GetTotalCollections() (collCount int64, err error) { SELECT COUNT(*) FROM collections c 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 { log.Error("Unable to fetch collections count: %v", err) } @@ -1662,7 +1662,7 @@ func (db *datastore) GetTotalPosts() (postCount int64, err error) { SELECT COUNT(*) FROM posts p 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 { 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) } - 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 { log.Error("Failed selecting from users: %v", err) 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{} for rows.Next() { 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 { log.Error("Failed scanning GetAllUsers() row: %v", err) break @@ -2431,10 +2431,11 @@ func (db *datastore) GetUserLastPostTime(id int64) (*time.Time, error) { return &t, nil } -func (db *datastore) SetUserSuspended(id int64, suspend bool) error { - _, err := db.Exec("UPDATE users SET suspended = ? WHERE id = ?", suspend, id) +// 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 suspended status: %v", err) + return fmt.Errorf("failed to update user status: %v", err) } return nil } diff --git a/invites.go b/invites.go index adff49a4..8f341eca 100644 --- a/invites.go +++ b/invites.go @@ -78,7 +78,7 @@ func handleCreateUserInvite(app *App, u *User, w http.ResponseWriter, r *http.Re muVal := r.FormValue("uses") expVal := r.FormValue("expires") - if u.Suspended { + if u.Status == UserSuspended { return ErrUserSuspended } diff --git a/migrations/migrations.go b/migrations/migrations.go index de3f4874..0799f8e0 100644 --- a/migrations/migrations.go +++ b/migrations/migrations.go @@ -58,7 +58,7 @@ func (m *migration) Migrate(db *datastore) error { var migrations = []Migration{ New("support user invites", supportUserInvites), // -> V1 (v0.8.0) New("support dynamic instance pages", supportInstancePages), // V1 -> V2 (v0.9.0) - New("support users suspension", supportUserSuspension), // V2 -> V3 () + New("support users suspension", supportUserStatus), // V2 -> V3 (v0.11.0) } // CurrentVer returns the current migration version the application is on diff --git a/migrations/v3.go b/migrations/v3.go index c7c00a92..b5351daf 100644 --- a/migrations/v3.go +++ b/migrations/v3.go @@ -10,10 +10,10 @@ package migrations -func supportUserSuspension(db *datastore) error { +func supportUserStatus(db *datastore) error { 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 { t.Rollback() return err diff --git a/posts.go b/posts.go index 33d46381..15d93c8d 100644 --- a/posts.go +++ b/posts.go @@ -383,7 +383,7 @@ 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) + log.Error("view post: %v", err) return ErrInternalGeneral } @@ -509,7 +509,7 @@ func newPost(app *App, w http.ResponseWriter, r *http.Request) error { } suspended, err := app.db.IsUserSuspended(userID) if err != nil { - log.Error("new post: get user: %v", err) + log.Error("new post: %v", err) return ErrInternalGeneral } if suspended { @@ -683,7 +683,7 @@ 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) + log.Error("existing post: %v", err) return ErrInternalGeneral } if suspended { @@ -886,7 +886,7 @@ func addPost(app *App, w http.ResponseWriter, r *http.Request) error { suspended, err := app.db.IsUserSuspended(ownerID) if err != nil { - log.Error("add post: get user: %v", err) + log.Error("add post: %v", err) return ErrInternalGeneral } if suspended { @@ -989,7 +989,7 @@ func pinPost(app *App, w http.ResponseWriter, r *http.Request) error { suspended, err := app.db.IsUserSuspended(userID) if err != nil { - log.Error("pin post: get user: %v", err) + log.Error("pin post: %v", err) return ErrInternalGeneral } if suspended { @@ -1063,7 +1063,7 @@ func fetchPost(app *App, w http.ResponseWriter, r *http.Request) error { } suspended, err := app.db.IsUserSuspended(ownerID) if err != nil { - log.Error("fetch post: get owner: %v", err) + log.Error("fetch post: %v", err) return ErrInternalGeneral } @@ -1333,13 +1333,10 @@ func viewCollectionPost(app *App, w http.ResponseWriter, r *http.Request) error suspended, err := app.db.IsUserSuspended(c.OwnerID) if err != nil { - log.Error("view collection post: get owner: %v", err) + log.Error("view collection post: %v", err) return ErrInternalGeneral } - if suspended { - return ErrPostNotFound - } // Check collection permissions if c.IsPrivate() && (u == nil || u.ID != c.OwnerID) { return ErrPostNotFound @@ -1396,6 +1393,9 @@ Are you sure it was ever here?`, 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."} @@ -1445,12 +1445,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) diff --git a/read.go b/read.go index 86664b5c..6d0c8a7d 100644 --- a/read.go +++ b/read.go @@ -71,7 +71,7 @@ func (app *App) FetchPublicPosts() (interface{}, error) { FROM collections c LEFT JOIN posts p ON p.collection_id = c.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`) if err != nil { log.Error("Failed selecting from posts: %v", err) diff --git a/routes.go b/routes.go index 510a5391..1ff250fb 100644 --- a/routes.go +++ b/routes.go @@ -144,7 +144,7 @@ func InitRoutes(apper Apper, r *mux.Router) *mux.Router { write.HandleFunc("/admin", handler.Admin(handleViewAdminDash)).Methods("GET") write.HandleFunc("/admin/users", handler.Admin(handleViewAdminUsers)).Methods("GET") write.HandleFunc("/admin/user/{username}", handler.Admin(handleViewAdminUser)).Methods("GET") - write.HandleFunc("/admin/user/{username}", handler.Admin(handleAdminToggleUserSuspended)).Methods("POST") + write.HandleFunc("/admin/user/{username}/status", handler.Admin(handleAdminToggleUserStatus)).Methods("POST") write.HandleFunc("/admin/pages", handler.Admin(handleViewAdminPages)).Methods("GET") write.HandleFunc("/admin/page/{slug}", handler.Admin(handleViewAdminPage)).Methods("GET") write.HandleFunc("/admin/update/config", handler.AdminApper(handleAdminUpdateConfig)).Methods("POST") diff --git a/schema.sql b/schema.sql index 3a797363..b3fae97a 100644 --- a/schema.sql +++ b/schema.sql @@ -225,7 +225,6 @@ CREATE TABLE IF NOT EXISTS `users` ( `password` char(60) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, `email` varbinary(255) DEFAULT NULL, `created` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, - `suspended` tinyint(1) NOT NULL DEFAULT 0, PRIMARY KEY (`id`), UNIQUE KEY `username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1; diff --git a/sqlite.sql b/sqlite.sql index 920ffeda..90989ed5 100644 --- a/sqlite.sql +++ b/sqlite.sql @@ -214,8 +214,7 @@ CREATE TABLE IF NOT EXISTS `users` ( username TEXT NOT NULL UNIQUE, password TEXT NOT NULL, email TEXT DEFAULT NULL, - created DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - suspended INTEGER NOT NULL DEFAULT 0 + created DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ); -- -------------------------------------------------------- diff --git a/templates.go b/templates.go index 6e9a0083..968845d7 100644 --- a/templates.go +++ b/templates.go @@ -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"), )) } diff --git a/templates/chorus-collection-post.tmpl b/templates/chorus-collection-post.tmpl index bab2e31f..58e514f0 100644 --- a/templates/chorus-collection-post.tmpl +++ b/templates/chorus-collection-post.tmpl @@ -65,6 +65,9 @@ article time.dt-published { {{template "user-navigation" .}} + {{if .Suspended}} + {{template "user-suspended"}} + {{end}}
{{if .IsScheduled}}

Scheduled

{{end}}{{if .Title.String}}

{{.FormattedDisplayTitle}}

{{end}}{{/* TODO: check format: if .Collection.Format.ShowDates*/}}
{{.HTMLContent}}
{{ if .Collection.ShowFooterBranding }} diff --git a/templates/chorus-collection.tmpl b/templates/chorus-collection.tmpl index e36d3b5e..5f4d418e 100644 --- a/templates/chorus-collection.tmpl +++ b/templates/chorus-collection.tmpl @@ -61,6 +61,9 @@ body#collection header nav.tabs a:first-child { {{template "user-navigation" .}} + {{if .Suspended}} + {{template "user-suspended"}} + {{end}}

{{.DisplayTitle}}

{{if .Description}}

{{.Description}}

{{end}} diff --git a/templates/collection-post.tmpl b/templates/collection-post.tmpl index 7075226a..43988041 100644 --- a/templates/collection-post.tmpl +++ b/templates/collection-post.tmpl @@ -59,6 +59,9 @@
+ {{if .Suspended}} + {{template "user-suspended"}} + {{end}}
{{if .IsScheduled}}

Scheduled

{{end}}{{if .Title.String}}

{{.FormattedDisplayTitle}}

{{end}}
{{.HTMLContent}}
{{ if .Collection.ShowFooterBranding }} diff --git a/templates/collection-tags.tmpl b/templates/collection-tags.tmpl index 7cad3b78..5e2e2d32 100644 --- a/templates/collection-tags.tmpl +++ b/templates/collection-tags.tmpl @@ -53,6 +53,9 @@ + {{if .Suspended}} + {{template "user-suspended"}} + {{end}} {{if .Posts}}
{{else}}
{{end}}

{{.Tag}}

{{template "posts" .}} diff --git a/templates/collection.tmpl b/templates/collection.tmpl index 36a266b7..5a33bbac 100644 --- a/templates/collection.tmpl +++ b/templates/collection.tmpl @@ -62,6 +62,9 @@ {{end}}
+ {{if .Suspended}} + {{template "user-suspended"}} + {{end}}

{{if .Posts}}{{else}}write.as {{end}}{{.DisplayTitle}}

{{if .Description}}

{{.Description}}

{{end}} {{/*if not .Public/*}} diff --git a/templates/password-collection.tmpl b/templates/password-collection.tmpl index e0b755d4..73c44653 100644 --- a/templates/password-collection.tmpl +++ b/templates/password-collection.tmpl @@ -25,6 +25,9 @@ + {{if .Suspended}} + {{template "user-supsended"}} + {{end}}

{{.DisplayTitle}}

diff --git a/templates/post.tmpl b/templates/post.tmpl index dd1375ec..74135a3e 100644 --- a/templates/post.tmpl +++ b/templates/post.tmpl @@ -36,6 +36,9 @@ + {{if .Suspended}} + {{template "user-suspended"}} + {{end}}

{{.SiteName}}

{{end}}
diff --git a/templates/collection-post.tmpl b/templates/collection-post.tmpl index 7075226a..a4084b3e 100644 --- a/templates/collection-post.tmpl +++ b/templates/collection-post.tmpl @@ -9,7 +9,7 @@ {{ if .IsFound }} - + @@ -26,7 +26,7 @@ - + {{range .Images}}{{else}}{{end}} @@ -50,7 +50,7 @@

diff --git a/templates/collection.tmpl b/templates/collection.tmpl index 36a266b7..fa66f691 100644 --- a/templates/collection.tmpl +++ b/templates/collection.tmpl @@ -68,7 +68,7 @@ {{/*end*/}} {{if .PinnedPosts}} + {{range .PinnedPosts}}{{.PlainDisplayTitle}}{{end}} {{end}} diff --git a/templates/read.tmpl b/templates/read.tmpl index 9541ab52..91fbeb40 100644 --- a/templates/read.tmpl +++ b/templates/read.tmpl @@ -87,17 +87,17 @@ {{ if gt (len .Posts) 0 }}
{{range .Posts}}
- {{if .Title.String}}

+ {{if .Title.String}}

{{else}} -

+

{{end}}

{{if .Collection}}from {{.Collection.DisplayTitle}}{{else}}Anonymous{{end}}

{{if .Excerpt}}
{{.Excerpt}}
- {{localstr "Read more..." .Language.String}}{{else}}
{{ if not .HTMLContent }}

{{.Content}}

{{ else }}{{.HTMLContent}}{{ end }}
 
+ {{localstr "Read more..." .Language.String}}{{else}}
{{ if not .HTMLContent }}

{{.Content}}

{{ else }}{{.HTMLContent}}{{ end }}
 
- {{localstr "Read more..." .Language.String}}{{end}}
+ {{localstr "Read more..." .Language.String}}{{end}} {{end}}
{{ else }} From f66d5bf1e8fa35330f52c2bb6b58f9720831029b Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Sat, 9 Nov 2019 11:41:39 -0800 Subject: [PATCH 11/31] use .Host instead of adding .Hostname --- posts.go | 2 -- templates/chorus-collection-post.tmpl | 6 +++--- templates/chorus-collection.tmpl | 2 +- templates/collection-post.tmpl | 6 +++--- templates/collection-tags.tmpl | 2 +- templates/collection.tmpl | 2 +- templates/read.tmpl | 8 ++++---- 7 files changed, 13 insertions(+), 15 deletions(-) diff --git a/posts.go b/posts.go index 6cb76a23..d0042968 100644 --- a/posts.go +++ b/posts.go @@ -1380,14 +1380,12 @@ Are you sure it was ever here?`, IsFound bool IsAdmin bool CanInvite bool - Hostname string }{ PublicPost: p, StaticPage: pageForReq(app, r), IsOwner: cr.isCollOwner, IsCustomDomain: cr.isCustomDomain, IsFound: postFound, - Hostname: app.cfg.App.Host, } tp.IsAdmin = u != nil && u.IsAdmin() tp.CanInvite = canUserInvite(app.cfg, tp.IsAdmin) diff --git a/templates/chorus-collection-post.tmpl b/templates/chorus-collection-post.tmpl index d229c621..18fb632f 100644 --- a/templates/chorus-collection-post.tmpl +++ b/templates/chorus-collection-post.tmpl @@ -8,7 +8,7 @@ - + @@ -25,7 +25,7 @@ - + {{range .Images}}{{else}}{{end}} @@ -77,7 +77,7 @@ article time.dt-published {


diff --git a/templates/chorus-collection.tmpl b/templates/chorus-collection.tmpl index ebee403f..14d5fbd1 100644 --- a/templates/chorus-collection.tmpl +++ b/templates/chorus-collection.tmpl @@ -68,7 +68,7 @@ body#collection header nav.tabs a:first-child { {{/*end*/}} {{if .PinnedPosts}} + {{range .PinnedPosts}}{{.PlainDisplayTitle}}{{end}} {{end}} diff --git a/templates/collection-post.tmpl b/templates/collection-post.tmpl index a4084b3e..4af5cb83 100644 --- a/templates/collection-post.tmpl +++ b/templates/collection-post.tmpl @@ -9,7 +9,7 @@ {{ if .IsFound }} - + @@ -26,7 +26,7 @@ - + {{range .Images}}{{else}}{{end}} @@ -50,7 +50,7 @@

{{end}} diff --git a/templates/read.tmpl b/templates/read.tmpl index 91fbeb40..f1cbf291 100644 --- a/templates/read.tmpl +++ b/templates/read.tmpl @@ -87,17 +87,17 @@ {{ if gt (len .Posts) 0 }}
{{range .Posts}}
- {{if .Title.String}}

+ {{if .Title.String}}

{{else}} -

+

{{end}}

{{if .Collection}}from {{.Collection.DisplayTitle}}{{else}}Anonymous{{end}}

{{if .Excerpt}}
{{.Excerpt}}
- {{localstr "Read more..." .Language.String}}{{else}}
{{ if not .HTMLContent }}

{{.Content}}

{{ else }}{{.HTMLContent}}{{ end }}
 
+ {{localstr "Read more..." .Language.String}}{{else}}
{{ if not .HTMLContent }}

{{.Content}}

{{ else }}{{.HTMLContent}}{{ end }}
 
- {{localstr "Read more..." .Language.String}}{{end}}
+ {{localstr "Read more..." .Language.String}}{{end}} {{end}}
{{ else }} From 2c2ee0c00cd80e199678ac53adac25d6ff5803a3 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Mon, 11 Nov 2019 15:16:04 +0900 Subject: [PATCH 12/31] Tweak "suspended" notification copy --- templates/user/include/suspended.tmpl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/templates/user/include/suspended.tmpl b/templates/user/include/suspended.tmpl index b1e42c82..e5d9be80 100644 --- a/templates/user/include/suspended.tmpl +++ b/templates/user/include/suspended.tmpl @@ -1,6 +1,5 @@ {{define "user-suspended"}}
-

This account is currently suspended.

-

Please contact the instance administrator to discuss reactivation.

+

Your account is suspended. You can still access all of your posts and blogs, but no one else can currently see them.

{{end}} From 6e09fcb9e2a3088c9c5ad1cbbbb5cc5947d2122a Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Mon, 11 Nov 2019 16:02:22 +0900 Subject: [PATCH 13/31] Change password reset endpoint to /admin/user/{Username}/passphrase Ref T695 --- routes.go | 2 +- templates/user/admin/view-user.tmpl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/routes.go b/routes.go index 003b7d17..de19ff25 100644 --- a/routes.go +++ b/routes.go @@ -144,7 +144,7 @@ func InitRoutes(apper Apper, r *mux.Router) *mux.Router { write.HandleFunc("/admin", handler.Admin(handleViewAdminDash)).Methods("GET") write.HandleFunc("/admin/users", handler.Admin(handleViewAdminUsers)).Methods("GET") write.HandleFunc("/admin/user/{username}", handler.Admin(handleViewAdminUser)).Methods("GET") - write.HandleFunc("/admin/user/{username}", handler.Admin(handleAdminResetUserPass)).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") write.HandleFunc("/admin/update/config", handler.AdminApper(handleAdminUpdateConfig)).Methods("POST") diff --git a/templates/user/admin/view-user.tmpl b/templates/user/admin/view-user.tmpl index 211297d6..91fdaf1d 100644 --- a/templates/user/admin/view-user.tmpl +++ b/templates/user/admin/view-user.tmpl @@ -62,7 +62,7 @@ button[type="submit"].danger { Password {{if not .OwnUserPage}} - +