Merge pull request #174 from writeas/T661-disable-accounts

Add account suspension features
fix-host-single-user-templates v0.11.0
Matt Baer 5 years ago committed by GitHub
commit bca678aee5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 29
      account.go
  2. 40
      activitypub.go
  3. 25
      admin.go
  4. 40
      collections.go
  5. 52
      database.go
  6. 5
      errors.go
  7. 14
      feed.go
  8. 4
      invites.go
  9. 9
      less/core.less
  10. 2
      migrations/migrations.go
  11. 29
      migrations/v3.go
  12. 11
      pad.go
  13. 78
      posts.go
  14. 14
      read.go
  15. 1
      routes.go
  16. 12
      templates.go
  17. 3
      templates/chorus-collection-post.tmpl
  18. 3
      templates/chorus-collection.tmpl
  19. 3
      templates/collection-post.tmpl
  20. 3
      templates/collection-tags.tmpl
  21. 3
      templates/collection.tmpl
  22. 4
      templates/edit-meta.tmpl
  23. 6
      templates/pad.tmpl
  24. 3
      templates/password-collection.tmpl
  25. 5
      templates/post.tmpl
  26. 2
      templates/user/admin/users.tmpl
  27. 37
      templates/user/admin/view-user.tmpl
  28. 3
      templates/user/articles.tmpl
  29. 3
      templates/user/collection.tmpl
  30. 3
      templates/user/collections.tmpl
  31. 5
      templates/user/include/suspended.tmpl
  32. 3
      templates/user/settings.tmpl
  33. 3
      templates/user/stats.tmpl
  34. 12
      users.go
  35. 11
      webfinger.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)
@ -1020,11 +1047,13 @@ func viewSettings(app *App, u *User, w http.ResponseWriter, r *http.Request) err
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",
Suspended: fullUser.IsSilenced(),
}
showUserPage(w, "settings", obj)

@ -80,6 +80,14 @@ func handleFetchCollectionActivities(app *App, w http.ResponseWriter, r *http.Re
if err != nil {
return err
}
suspended, err := app.db.IsUserSuspended(c.OwnerID)
if err != nil {
log.Error("fetch collection activities: %v", err)
return ErrInternalGeneral
}
if suspended {
return ErrCollectionNotFound
}
c.hostName = app.cfg.App.Host
p := c.PersonObject()
@ -105,6 +113,14 @@ func handleFetchCollectionOutbox(app *App, w http.ResponseWriter, r *http.Reques
if err != nil {
return err
}
suspended, err := app.db.IsUserSuspended(c.OwnerID)
if err != nil {
log.Error("fetch collection outbox: %v", err)
return ErrInternalGeneral
}
if suspended {
return ErrCollectionNotFound
}
c.hostName = app.cfg.App.Host
if app.cfg.App.SingleUser {
@ -158,6 +174,14 @@ func handleFetchCollectionFollowers(app *App, w http.ResponseWriter, r *http.Req
if err != nil {
return err
}
suspended, err := app.db.IsUserSuspended(c.OwnerID)
if err != nil {
log.Error("fetch collection followers: %v", err)
return ErrInternalGeneral
}
if suspended {
return ErrCollectionNotFound
}
c.hostName = app.cfg.App.Host
accountRoot := c.FederatedAccount()
@ -204,6 +228,14 @@ func handleFetchCollectionFollowing(app *App, w http.ResponseWriter, r *http.Req
if err != nil {
return err
}
suspended, err := app.db.IsUserSuspended(c.OwnerID)
if err != nil {
log.Error("fetch collection following: %v", err)
return ErrInternalGeneral
}
if suspended {
return ErrCollectionNotFound
}
c.hostName = app.cfg.App.Host
accountRoot := c.FederatedAccount()
@ -238,6 +270,14 @@ func handleFetchCollectionInbox(app *App, w http.ResponseWriter, r *http.Request
// TODO: return Reject?
return err
}
suspended, err := app.db.IsUserSuspended(c.OwnerID)
if err != nil {
log.Error("fetch collection inbox: %v", err)
return ErrInternalGeneral
}
if suspended {
return ErrCollectionNotFound
}
c.hostName = app.cfg.App.Host
if debugging {

@ -241,12 +241,37 @@ func handleViewAdminUser(app *App, u *User, w http.ResponseWriter, r *http.Reque
return nil
}
func handleAdminToggleUserStatus(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r)
username := vars["username"]
if username == "" {
return impart.HTTPError{http.StatusFound, "/admin/users"}
}
user, err := app.db.GetUserForAuth(username)
if err != nil {
log.Error("failed to get user: %v", err)
return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get user from username: %v", err)}
}
if user.IsSilenced() {
err = app.db.SetUserStatus(user.ID, UserActive)
} else {
err = app.db.SetUserStatus(user.ID, UserSilenced)
}
if err != nil {
log.Error("toggle user suspended: %v", err)
return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not toggle user status: %v")}
}
return impart.HTTPError{http.StatusFound, fmt.Sprintf("/admin/user/%s#status", username)}
}
func handleAdminResetUserPass(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r)
username := vars["username"]
if username == "" {
return impart.HTTPError{http.StatusFound, "/admin/users"}
}
// Generate new random password since none supplied
pass := passgen.NewWordish()
hashedPass, err := auth.HashPass([]byte(pass))

@ -71,6 +71,7 @@ type (
CurrentPage int
TotalPages int
Format *CollectionFormat
Suspended bool
}
SubmittedCollection struct {
// Data used for updating a given collection
@ -379,6 +380,7 @@ func newCollection(app *App, w http.ResponseWriter, r *http.Request) error {
}
var userID int64
var err error
if reqJSON && !c.Web {
accessToken = r.Header.Get("Authorization")
if accessToken == "" {
@ -395,6 +397,14 @@ func newCollection(app *App, w http.ResponseWriter, r *http.Request) error {
}
userID = u.ID
}
suspended, err := app.db.IsUserSuspended(userID)
if err != nil {
log.Error("new collection: %v", err)
return ErrInternalGeneral
}
if suspended {
return ErrUserSuspended
}
if !author.IsValidUsername(app.cfg, c.Alias) {
return impart.HTTPError{http.StatusPreconditionFailed, "Collection alias isn't valid."}
@ -477,6 +487,7 @@ func fetchCollection(app *App, w http.ResponseWriter, r *http.Request) error {
res.Owner = u
}
}
// TODO: check suspended
app.db.GetPostsCount(res, isCollOwner)
// Strip non-public information
res.Collection.ForPublic()
@ -725,9 +736,14 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro
if c == nil || err != nil {
return err
}
c.hostName = app.cfg.App.Host
suspended, err := app.db.IsUserSuspended(c.OwnerID)
if err != nil {
log.Error("view collection: %v", err)
return ErrInternalGeneral
}
// Serve ActivityStreams data now, if requested
if strings.Contains(r.Header.Get("Accept"), "application/activity+json") {
ac := c.PersonObject()
@ -784,6 +800,10 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro
log.Error("Error getting user for collection: %v", err)
}
}
if !isOwner && suspended {
return ErrCollectionNotFound
}
displayPage.Suspended = isOwner && suspended
displayPage.Owner = owner
coll.Owner = displayPage.Owner
@ -886,6 +906,10 @@ func handleViewCollectionTag(app *App, w http.ResponseWriter, r *http.Request) e
log.Error("Error getting user for collection: %v", err)
}
}
if !isOwner && u.IsSilenced() {
return ErrCollectionNotFound
}
displayPage.Suspended = u.IsSilenced()
displayPage.Owner = owner
coll.Owner = displayPage.Owner
// Add more data
@ -924,11 +948,10 @@ func existingCollection(app *App, w http.ResponseWriter, r *http.Request) error
collAlias := vars["alias"]
isWeb := r.FormValue("web") == "1"
var u *User
u := &User{}
if reqJSON && !isWeb {
// Ensure an access token was given
accessToken := r.Header.Get("Authorization")
u = &User{}
u.ID = app.db.GetUserID(accessToken)
if u.ID == -1 {
return ErrBadAccessToken
@ -940,6 +963,16 @@ func existingCollection(app *App, w http.ResponseWriter, r *http.Request) error
}
}
suspended, err := app.db.IsUserSuspended(u.ID)
if err != nil {
log.Error("existing collection: %v", err)
return ErrInternalGeneral
}
if suspended {
return ErrUserSuspended
}
if r.Method == "DELETE" {
err := app.db.DeleteCollection(collAlias, u.ID)
if err != nil {
@ -952,7 +985,6 @@ func existingCollection(app *App, w http.ResponseWriter, r *http.Request) error
}
c := SubmittedCollection{OwnerID: uint64(u.ID)}
var err error
if reqJSON {
// Decode JSON request

@ -296,7 +296,7 @@ func (db *datastore) CreateCollection(cfg *config.Config, alias, title string, u
func (db *datastore) GetUserByID(id int64) (*User, error) {
u := &User{ID: id}
err := db.QueryRow("SELECT username, password, email, created FROM users WHERE id = ?", id).Scan(&u.Username, &u.HashedPass, &u.Email, &u.Created)
err := db.QueryRow("SELECT username, password, email, created, status FROM users WHERE id = ?", id).Scan(&u.Username, &u.HashedPass, &u.Email, &u.Created, &u.Status)
switch {
case err == sql.ErrNoRows:
return nil, ErrUserNotFound
@ -308,6 +308,23 @@ func (db *datastore) GetUserByID(id int64) (*User, error) {
return u, nil
}
// IsUserSuspended returns true if the user account associated with id is
// currently suspended.
func (db *datastore) IsUserSuspended(id int64) (bool, error) {
u := &User{ID: id}
err := db.QueryRow("SELECT status FROM users WHERE id = ?", id).Scan(&u.Status)
switch {
case err == sql.ErrNoRows:
return false, fmt.Errorf("is user suspended: %v", ErrUserNotFound)
case err != nil:
log.Error("Couldn't SELECT user password: %v", err)
return false, fmt.Errorf("is user suspended: %v", err)
}
return u.IsSilenced(), nil
}
// DoesUserNeedAuth returns true if the user hasn't provided any methods for
// authenticating with the account, such a passphrase or email address.
// Any errors are reported to admin and silently quashed, returning false as the
@ -347,7 +364,7 @@ func (db *datastore) IsUserPassSet(id int64) (bool, error) {
func (db *datastore) GetUserForAuth(username string) (*User, error) {
u := &User{Username: username}
err := db.QueryRow("SELECT id, password, email, created FROM users WHERE username = ?", username).Scan(&u.ID, &u.HashedPass, &u.Email, &u.Created)
err := db.QueryRow("SELECT id, password, email, created, status FROM users WHERE username = ?", username).Scan(&u.ID, &u.HashedPass, &u.Email, &u.Created, &u.Status)
switch {
case err == sql.ErrNoRows:
// Check if they've entered the wrong, unnormalized username
@ -370,7 +387,7 @@ func (db *datastore) GetUserForAuth(username string) (*User, error) {
func (db *datastore) GetUserForAuthByID(userID int64) (*User, error) {
u := &User{ID: userID}
err := db.QueryRow("SELECT id, password, email, created FROM users WHERE id = ?", u.ID).Scan(&u.ID, &u.HashedPass, &u.Email, &u.Created)
err := db.QueryRow("SELECT id, password, email, created, status FROM users WHERE id = ?", u.ID).Scan(&u.ID, &u.HashedPass, &u.Email, &u.Created, &u.Status)
switch {
case err == sql.ErrNoRows:
return nil, ErrUserNotFound
@ -1629,7 +1646,11 @@ func (db *datastore) GetMeStats(u *User) userMeStats {
}
func (db *datastore) GetTotalCollections() (collCount int64, err error) {
err = db.QueryRow(`SELECT COUNT(*) FROM collections`).Scan(&collCount)
err = db.QueryRow(`
SELECT COUNT(*)
FROM collections c
LEFT JOIN users u ON u.id = c.owner_id
WHERE u.status = 0`).Scan(&collCount)
if err != nil {
log.Error("Unable to fetch collections count: %v", err)
}
@ -1637,7 +1658,11 @@ func (db *datastore) GetTotalCollections() (collCount int64, err error) {
}
func (db *datastore) GetTotalPosts() (postCount int64, err error) {
err = db.QueryRow(`SELECT COUNT(*) FROM posts`).Scan(&postCount)
err = db.QueryRow(`
SELECT COUNT(*)
FROM posts p
LEFT JOIN users u ON u.id = p.owner_id
WHERE u.status = 0`).Scan(&postCount)
if err != nil {
log.Error("Unable to fetch posts count: %v", err)
}
@ -2359,17 +2384,17 @@ func (db *datastore) GetAllUsers(page uint) (*[]User, error) {
limitStr = fmt.Sprintf("%d, %d", (page-1)*adminUsersPerPage, adminUsersPerPage)
}
rows, err := db.Query("SELECT id, username, created FROM users ORDER BY created DESC LIMIT " + limitStr)
rows, err := db.Query("SELECT id, username, created, status FROM users ORDER BY created DESC LIMIT " + limitStr)
if err != nil {
log.Error("Failed selecting from posts: %v", err)
return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve user posts."}
log.Error("Failed selecting from users: %v", err)
return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve all users."}
}
defer rows.Close()
users := []User{}
for rows.Next() {
u := User{}
err = rows.Scan(&u.ID, &u.Username, &u.Created)
err = rows.Scan(&u.ID, &u.Username, &u.Created, &u.Status)
if err != nil {
log.Error("Failed scanning GetAllUsers() row: %v", err)
break
@ -2406,6 +2431,15 @@ func (db *datastore) GetUserLastPostTime(id int64) (*time.Time, error) {
return &t, nil
}
// SetUserStatus changes a user's status in the database. see Users.UserStatus
func (db *datastore) SetUserStatus(id int64, status UserStatus) error {
_, err := db.Exec("UPDATE users SET status = ? WHERE id = ?", status, id)
if err != nil {
return fmt.Errorf("failed to update user status: %v", err)
}
return nil
}
func (db *datastore) GetCollectionLastPostTime(id int64) (*time.Time, error) {
var t time.Time
err := db.QueryRow("SELECT created FROM posts WHERE collection_id = ? ORDER BY created DESC LIMIT 1", id).Scan(&t)

@ -11,8 +11,9 @@
package writefreely
import (
"github.com/writeas/impart"
"net/http"
"github.com/writeas/impart"
)
// Commonly returned HTTP errors
@ -46,6 +47,8 @@ var (
ErrUserNotFound = impart.HTTPError{http.StatusNotFound, "User doesn't exist."}
ErrUserNotFoundEmail = impart.HTTPError{http.StatusNotFound, "Please enter your username instead of your email address."}
ErrUserSuspended = impart.HTTPError{http.StatusForbidden, "Account is silenced."}
)
// Post operation errors

@ -12,12 +12,13 @@ package writefreely
import (
"fmt"
"net/http"
"time"
. "github.com/gorilla/feeds"
"github.com/gorilla/mux"
stripmd "github.com/writeas/go-strip-markdown"
"github.com/writeas/web-core/log"
"net/http"
"time"
)
func ViewFeed(app *App, w http.ResponseWriter, req *http.Request) error {
@ -34,6 +35,15 @@ func ViewFeed(app *App, w http.ResponseWriter, req *http.Request) error {
if err != nil {
return nil
}
suspended, err := app.db.IsUserSuspended(c.OwnerID)
if err != nil {
log.Error("view feed: get user: %v", err)
return ErrInternalGeneral
}
if suspended {
return ErrCollectionNotFound
}
c.hostName = app.cfg.App.Host
if c.IsPrivate() || c.IsProtected() {

@ -78,6 +78,10 @@ func handleCreateUserInvite(app *App, u *User, w http.ResponseWriter, r *http.Re
muVal := r.FormValue("uses")
expVal := r.FormValue("expires")
if u.IsSilenced() {
return ErrUserSuspended
}
var err error
var maxUses int
if muVal != "0" {

@ -516,10 +516,17 @@ abbr {
body#collection article p, body#subpage article p {
.article-p;
}
pre, body#post article, body#collection article, body#subpage article, body#subpage #wrapper h1 {
pre, body#post article, #post .alert, #subpage .alert, body#collection article, body#subpage article, body#subpage #wrapper h1 {
max-width: 40rem;
margin: 0 auto;
}
#collection header .alert, #post .alert, #subpage .alert {
margin-bottom: 1em;
p {
text-align: left;
line-height: 1.4;
}
}
textarea, pre, body#post article, body#collection article p {
&.norm, &.sans, &.wrap {
line-height: 1.4em;

@ -13,6 +13,7 @@ package migrations
import (
"database/sql"
"github.com/writeas/web-core/log"
)
@ -57,6 +58,7 @@ func (m *migration) Migrate(db *datastore) error {
var migrations = []Migration{
New("support user invites", supportUserInvites), // -> V1 (v0.8.0)
New("support dynamic instance pages", supportInstancePages), // V1 -> V2 (v0.9.0)
New("support users suspension", supportUserStatus), // V2 -> V3 (v0.11.0)
}
// CurrentVer returns the current migration version the application is on

@ -0,0 +1,29 @@
/*
* Copyright © 2019 A Bunch Tell LLC.
*
* This file is part of WriteFreely.
*
* WriteFreely is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, included
* in the LICENSE file in this source code package.
*/
package migrations
func supportUserStatus(db *datastore) error {
t, err := db.Begin()
_, err = t.Exec(`ALTER TABLE users ADD COLUMN status ` + db.typeInt() + ` DEFAULT '0' NOT NULL`)
if err != nil {
t.Rollback()
return err
}
err = t.Commit()
if err != nil {
t.Rollback()
return err
}
return nil
}

@ -38,6 +38,7 @@ func handleViewPad(app *App, w http.ResponseWriter, r *http.Request) error {
Post *RawPost
User *User
Blogs *[]Collection
Suspended bool
Editing bool // True if we're modifying an existing post
EditCollection *Collection // Collection of the post we're editing, if any
@ -52,6 +53,10 @@ func handleViewPad(app *App, w http.ResponseWriter, r *http.Request) error {
if err != nil {
log.Error("Unable to get user's blogs for Pad: %v", err)
}
appData.Suspended, err = app.db.IsUserSuspended(appData.User.ID)
if err != nil {
log.Error("Unable to get users suspension status for Pad: %v", err)
}
}
padTmpl := app.cfg.App.Editor
@ -119,12 +124,18 @@ func handleViewMeta(app *App, w http.ResponseWriter, r *http.Request) error {
EditCollection *Collection // Collection of the post we're editing, if any
Flashes []string
NeedsToken bool
Suspended bool
}{
StaticPage: pageForReq(app, r),
Post: &RawPost{Font: "norm"},
User: getUserSession(app, r),
}
var err error
appData.Suspended, err = app.db.IsUserSuspended(appData.User.ID)
if err != nil {
log.Error("view meta: get user suspended status: %v", err)
return ErrInternalGeneral
}
if action == "" && slug == "" {
return ErrPostNotFound

@ -381,6 +381,12 @@ func handleViewPost(app *App, w http.ResponseWriter, r *http.Request) error {
}
}
suspended, err := app.db.IsUserSuspended(ownerID.Int64)
if err != nil {
log.Error("view post: %v", err)
return ErrInternalGeneral
}
// Check if post has been unpublished
if content == "" {
gone = true
@ -431,6 +437,7 @@ func handleViewPost(app *App, w http.ResponseWriter, r *http.Request) error {
Username string
IsOwner bool
SiteURL string
Suspended bool
}{
AnonymousPost: post,
StaticPage: pageForReq(app, r),
@ -441,6 +448,10 @@ func handleViewPost(app *App, w http.ResponseWriter, r *http.Request) error {
page.IsOwner = ownerID.Valid && ownerID.Int64 == u.ID
}
if !page.IsOwner && suspended {
return ErrPostNotFound
}
page.Suspended = suspended
err = templates["post"].ExecuteTemplate(w, "post", page)
if err != nil {
log.Error("Post template execute error: %v", err)
@ -497,6 +508,15 @@ func newPost(app *App, w http.ResponseWriter, r *http.Request) error {
} else {
userID = app.db.GetUserID(accessToken)
}
suspended, err := app.db.IsUserSuspended(userID)
if err != nil {
log.Error("new post: %v", err)
return ErrInternalGeneral
}
if suspended {
return ErrUserSuspended
}
if userID == -1 {
return ErrNotLoggedIn
}
@ -509,7 +529,7 @@ func newPost(app *App, w http.ResponseWriter, r *http.Request) error {
var p *SubmittedPost
if reqJSON {
decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&p)
err = decoder.Decode(&p)
if err != nil {
log.Error("Couldn't parse new post JSON request: %v\n", err)
return ErrBadJSON
@ -555,7 +575,6 @@ func newPost(app *App, w http.ResponseWriter, r *http.Request) error {
var newPost *PublicPost = &PublicPost{}
var coll *Collection
var err error
if accessToken != "" {
newPost, err = app.db.CreateOwnedPost(p, accessToken, collAlias, app.cfg.App.Host)
} else {
@ -663,6 +682,15 @@ func existingPost(app *App, w http.ResponseWriter, r *http.Request) error {
}
}
suspended, err := app.db.IsUserSuspended(userID)
if err != nil {
log.Error("existing post: %v", err)
return ErrInternalGeneral
}
if suspended {
return ErrUserSuspended
}
// Modify post struct
p.ID = postID
@ -857,11 +885,20 @@ func addPost(app *App, w http.ResponseWriter, r *http.Request) error {
ownerID = u.ID
}
suspended, err := app.db.IsUserSuspended(ownerID)
if err != nil {
log.Error("add post: %v", err)
return ErrInternalGeneral
}
if suspended {
return ErrUserSuspended
}
// Parse claimed posts in format:
// [{"id": "...", "token": "..."}]
var claims *[]ClaimPostRequest
decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&claims)
err = decoder.Decode(&claims)
if err != nil {
return ErrBadJSONArray
}
@ -951,13 +988,22 @@ func pinPost(app *App, w http.ResponseWriter, r *http.Request) error {
userID = u.ID
}
suspended, err := app.db.IsUserSuspended(userID)
if err != nil {
log.Error("pin post: %v", err)
return ErrInternalGeneral
}
if suspended {
return ErrUserSuspended
}
// Parse request
var posts []struct {
ID string `json:"id"`
Position int64 `json:"position"`
}
decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&posts)
err = decoder.Decode(&posts)
if err != nil {
return ErrBadJSONArray
}
@ -993,6 +1039,7 @@ func pinPost(app *App, w http.ResponseWriter, r *http.Request) error {
func fetchPost(app *App, w http.ResponseWriter, r *http.Request) error {
var collID int64
var ownerID int64
var coll *Collection
var err error
vars := mux.Vars(r)
@ -1008,12 +1055,22 @@ func fetchPost(app *App, w http.ResponseWriter, r *http.Request) error {
return err
}
collID = coll.ID
ownerID = coll.OwnerID
}
p, err := app.db.GetPost(vars["post"], collID)
if err != nil {
return err
}
suspended, err := app.db.IsUserSuspended(ownerID)
if err != nil {
log.Error("fetch post: %v", err)
return ErrInternalGeneral
}
if suspended {
return ErrPostNotFound
}
p.extractData()
@ -1275,6 +1332,12 @@ func viewCollectionPost(app *App, w http.ResponseWriter, r *http.Request) error
}
c.hostName = app.cfg.App.Host
suspended, err := app.db.IsUserSuspended(c.OwnerID)
if err != nil {
log.Error("view collection post: %v", err)
return ErrInternalGeneral
}
// Check collection permissions
if c.IsPrivate() && (u == nil || u.ID != c.OwnerID) {
return ErrPostNotFound
@ -1327,10 +1390,13 @@ Are you sure it was ever here?`,
return err
}
}
p.IsOwner = owner != nil && p.OwnerID.Valid && owner.ID == p.OwnerID.Int64
p.IsOwner = owner != nil && p.OwnerID.Valid && u.ID == p.OwnerID.Int64
p.Collection = coll
p.IsTopLevel = app.cfg.App.SingleUser
if !p.IsOwner && suspended {
return ErrPostNotFound
}
// Check if post has been unpublished
if p.Content == "" && p.Title.String == "" {
return impart.HTTPError{http.StatusGone, "Post was unpublished."}
@ -1380,12 +1446,14 @@ Are you sure it was ever here?`,
IsFound bool
IsAdmin bool
CanInvite bool
Suspended bool
}{
PublicPost: p,
StaticPage: pageForReq(app, r),
IsOwner: cr.isCollOwner,
IsCustomDomain: cr.isCustomDomain,
IsFound: postFound,
Suspended: suspended,
}
tp.IsAdmin = u != nil && u.IsAdmin()
tp.CanInvite = canUserInvite(app.cfg, tp.IsAdmin)

@ -13,6 +13,12 @@ package writefreely
import (
"database/sql"
"fmt"
"html/template"
"math"
"net/http"
"strconv"
"time"
. "github.com/gorilla/feeds"
"github.com/gorilla/mux"
stripmd "github.com/writeas/go-strip-markdown"
@ -20,11 +26,6 @@ import (
"github.com/writeas/web-core/log"
"github.com/writeas/web-core/memo"
"github.com/writeas/writefreely/page"
"html/template"
"math"
"net/http"
"strconv"
"time"
)
const (
@ -69,7 +70,8 @@ func (app *App) FetchPublicPosts() (interface{}, error) {
rows, err := app.db.Query(`SELECT p.id, alias, c.title, p.slug, p.title, p.content, p.text_appearance, p.language, p.rtl, p.created, p.updated
FROM collections c
LEFT JOIN posts p ON p.collection_id = c.id
WHERE c.privacy = 1 AND (p.created >= ` + app.db.dateSub(3, "month") + ` AND p.created <= ` + app.db.now() + ` AND pinned_position IS NULL)
LEFT JOIN users u ON u.id = p.owner_id
WHERE c.privacy = 1 AND (p.created >= ` + app.db.dateSub(3, "month") + ` AND p.created <= ` + app.db.now() + ` AND pinned_position IS NULL) AND u.status = 0
ORDER BY p.created DESC`)
if err != nil {
log.Error("Failed selecting from posts: %v", err)

@ -144,6 +144,7 @@ func InitRoutes(apper Apper, r *mux.Router) *mux.Router {
write.HandleFunc("/admin", handler.Admin(handleViewAdminDash)).Methods("GET")
write.HandleFunc("/admin/users", handler.Admin(handleViewAdminUsers)).Methods("GET")
write.HandleFunc("/admin/user/{username}", handler.Admin(handleViewAdminUser)).Methods("GET")
write.HandleFunc("/admin/user/{username}/status", handler.Admin(handleAdminToggleUserStatus)).Methods("POST")
write.HandleFunc("/admin/user/{username}/passphrase", handler.Admin(handleAdminResetUserPass)).Methods("POST")
write.HandleFunc("/admin/pages", handler.Admin(handleViewAdminPages)).Methods("GET")
write.HandleFunc("/admin/page/{slug}", handler.Admin(handleViewAdminPage)).Methods("GET")

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

@ -65,6 +65,9 @@ article time.dt-published {
{{template "user-navigation" .}}
{{if .Suspended}}
{{template "user-suspended"}}
{{end}}
<article id="post-body" class="{{.Font}} h-entry">{{if .IsScheduled}}<p class="badge">Scheduled</p>{{end}}{{if .Title.String}}<h2 id="title" class="p-name">{{.FormattedDisplayTitle}}</h2>{{end}}{{/* TODO: check format: if .Collection.Format.ShowDates*/}}<time class="dt-published" datetime="{{.Created}}" pubdate itemprop="datePublished" content="{{.Created}}">{{.DisplayDate}}</time><div class="e-content">{{.HTMLContent}}</div></article>
{{ if .Collection.ShowFooterBranding }}

@ -61,6 +61,9 @@ body#collection header nav.tabs a:first-child {
<body id="collection" itemscope itemtype="http://schema.org/WebPage">
{{template "user-navigation" .}}
{{if .Suspended}}
{{template "user-suspended"}}
{{end}}
<header>
<h1 dir="{{.Direction}}" id="blog-title"><a href="/{{if .IsTopLevel}}{{else}}{{.Prefix}}{{.Alias}}/{{end}}" class="h-card p-author u-url" rel="me author">{{.DisplayTitle}}</a></h1>
{{if .Description}}<p class="description p-note">{{.Description}}</p>{{end}}

@ -59,6 +59,9 @@
</nav>
</header>
{{if .Suspended}}
{{template "user-suspended"}}
{{end}}
<article id="post-body" class="{{.Font}} h-entry {{if not .IsFound}}error-page{{end}}">{{if .IsScheduled}}<p class="badge">Scheduled</p>{{end}}{{if .Title.String}}<h2 id="title" class="p-name">{{.FormattedDisplayTitle}}</h2>{{end}}<div class="e-content">{{.HTMLContent}}</div></article>
{{ if .Collection.ShowFooterBranding }}

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

@ -62,6 +62,9 @@
</ul></nav>{{end}}
<header>
{{if .Suspended}}
{{template "user-suspended"}}
{{end}}
<h1 dir="{{.Direction}}" id="blog-title">{{if .Posts}}{{else}}<span class="writeas-prefix"><a href="/">write.as</a></span> {{end}}<a href="/{{if .IsTopLevel}}{{else}}{{.Prefix}}{{.Alias}}/{{end}}" class="h-card p-author u-url" rel="me author">{{.DisplayTitle}}</a></h1>
{{if .Description}}<p class="description p-note">{{.Description}}</p>{{end}}
{{/*if not .Public/*}}

@ -269,6 +269,10 @@
<script src="/js/h.js"></script>
<script>
function updateMeta() {
if ({{.Suspended}}) {
alert('Your account is currently supsended, editing posts is disabled.');
return
}
document.getElementById('create-error').style.display = 'none';
var $created = document.getElementById('created');
var dateStr = $created.value.trim();

@ -131,8 +131,12 @@
{{else}}var canPublish = true;{{end}}
var publishing = false;
var justPublished = false;
var suspended = {{.Suspended}};
var publish = function(content, font) {
if (suspended === true) {
alert("Your account is silenced, so you can't publish or update posts.");
return;
}
{{if and (and .Post.Id (not .Post.Slug)) (not .User)}}
if (!token) {
alert("You don't have permission to update this post.");

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

@ -35,7 +35,6 @@
{{template "highlighting" .}}
</head>
<body id="post">
<header>
<h1 dir="{{.Direction}}"><a href="/">{{.SiteName}}</a></h1>
<nav>
@ -50,6 +49,10 @@
</nav>
</header>
{{if .Suspended}}
{{template "user-suspended"}}
{{end}}
<article class="{{.Font}} h-entry">{{if .Title}}<h2 id="title" class="p-name">{{.Title}}</h2>{{end}}{{ if .IsPlainText }}<p id="post-body" class="e-content">{{.Content}}</p>{{ else }}<div id="post-body" class="e-content">{{.HTMLContent}}</div>{{ end }}</article>
<footer dir="ltr"><hr><nav><p style="font-size: 0.9em">{{localhtml "published with write.as" .Language}}</p></nav></footer>

@ -11,12 +11,14 @@
<th>User</th>
<th>Joined</th>
<th>Type</th>
<th>Status</th>
</tr>
{{range .Users}}
<tr>
<td><a href="/admin/user/{{.Username}}">{{.Username}}</a></td>
<td>{{.CreatedFriendly}}</td>
<td style="text-align:center">{{if .IsAdmin}}Admin{{else}}User{{end}}</td>
<td style="text-align:center">{{if .IsSilenced}}Silenced{{else}}Active{{end}}</td>
</tr>
{{end}}
</table>

@ -7,6 +7,24 @@ table.classy th {
h3 {
font-weight: normal;
}
td.active-suspend {
display: flex;
align-items: center;
}
td.active-suspend > input[type="submit"] {
margin-left: auto;
margin-right: 5%;
}
@media only screen and (max-width: 500px) {
td.active-suspend {
flex-wrap: wrap;
}
td.active-suspend > input[type="submit"] {
margin: auto;
}
}
input.copy-text {
text-align: center;
font-size: 1.2em;
@ -51,6 +69,21 @@ input.copy-text {
<th>Last Post</th>
<td>{{if .LastPost}}{{.LastPost}}{{else}}Never{{end}}</td>
</tr>
<tr>
<form action="/admin/user/{{.User.Username}}/status" method="POST" {{if not .User.IsSilenced}}onsubmit="return confirmSilence()"{{end}}>
<a id="status"/>
<th>Status</th>
<td class="active-suspend">
{{if .User.IsSilenced}}
<p>Silenced</p>
<input type="submit" value="Unsilence"/>
{{else}}
<p>Active</p>
<input class="danger" type="submit" value="Silence" {{if .User.IsAdmin}}disabled{{end}}/>
{{end}}
</td>
</form>
</tr>
<tr>
<th>Password</th>
<td>
@ -110,6 +143,10 @@ input.copy-text {
</div>
<script type="text/javascript">
function confirmSilence() {
return confirm("Silence this user? They'll still be able to log in and access their posts, but no one else will be able to see them anymore. You can reverse this decision at any time.");
}
form = document.getElementById("reset-form");
form.addEventListener('submit', function(e) {
e.preventDefault();

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

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

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

@ -0,0 +1,5 @@
{{define "user-suspended"}}
<div class="alert info">
<p><strong>Your account has been silenced.</strong> You can still access all of your posts and blogs, but no one else can currently see them.</p>
</div>
{{end}}

@ -7,6 +7,9 @@ h3 { font-weight: normal; }
.section > *:not(input) { font-size: 0.86em; }
</style>
<div class="content-container snug regular">
{{if .Suspended}}
{{template "user-suspended"}}
{{end}}
<h2>{{if .IsLogOut}}Before you go...{{else}}Account Settings {{if .IsAdmin}}<a href="/admin">admin settings</a>{{end}}{{end}}</h2>
{{if .Flashes}}<ul class="errors">
{{range .Flashes}}<li class="urgent">{{.}}</li>{{end}}

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

@ -19,6 +19,13 @@ import (
"github.com/writeas/writefreely/key"
)
type UserStatus int
const (
UserActive = iota
UserSilenced
)
type (
userCredentials struct {
Alias string `json:"alias" schema:"alias"`
@ -59,6 +66,7 @@ type (
HasPass bool `json:"has_pass"`
Email zero.String `json:"email"`
Created time.Time `json:"created"`
Status UserStatus `json:"status"`
clearEmail string `json:"email"`
}
@ -118,3 +126,7 @@ func (u *User) IsAdmin() bool {
// TODO: get this from database
return u.ID == 1
}
func (u *User) IsSilenced() bool {
return u.Status&UserSilenced != 0
}

@ -11,11 +11,12 @@
package writefreely
import (
"net/http"
"github.com/writeas/go-webfinger"
"github.com/writeas/impart"
"github.com/writeas/web-core/log"
"github.com/writeas/writefreely/config"
"net/http"
)
type wfResolver struct {
@ -37,6 +38,14 @@ func (wfr wfResolver) FindUser(username string, host, requestHost string, r []we
log.Error("Unable to get blog: %v", err)
return nil, err
}
suspended, err := wfr.db.IsUserSuspended(c.OwnerID)
if err != nil {
log.Error("webfinger find user: check is suspended: %v", err)
return nil, err
}
if suspended {
return nil, wfUserNotFoundErr
}
c.hostName = wfr.cfg.App.Host
if wfr.cfg.App.SingleUser {
// Ensure handle matches user-chosen one on single-user blogs

Loading…
Cancel
Save