Kaynağa Gözat

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

Add account suspension features
tags/v0.11.0^0
Matt Baer 4 yıl önce
committed by GitHub
ebeveyn
işleme
bca678aee5
Veri tabanında bu imza için bilinen anahtar bulunamadı GPG Anahtar Kimliği: 4AEE18F83AFDEB23
35 değiştirilmiş dosya ile 458 ekleme ve 48 silme
  1. +36
    -7
      account.go
  2. +40
    -0
      activitypub.go
  3. +25
    -0
      admin.go
  4. +36
    -4
      collections.go
  5. +43
    -9
      database.go
  6. +4
    -1
      errors.go
  7. +12
    -2
      feed.go
  8. +4
    -0
      invites.go
  9. +8
    -1
      less/core.less
  10. +2
    -0
      migrations/migrations.go
  11. +29
    -0
      migrations/v3.go
  12. +14
    -3
      pad.go
  13. +76
    -8
      posts.go
  14. +8
    -6
      read.go
  15. +1
    -0
      routes.go
  16. +8
    -4
      templates.go
  17. +3
    -0
      templates/chorus-collection-post.tmpl
  18. +3
    -0
      templates/chorus-collection.tmpl
  19. +3
    -0
      templates/collection-post.tmpl
  20. +3
    -0
      templates/collection-tags.tmpl
  21. +3
    -0
      templates/collection.tmpl
  22. +4
    -0
      templates/edit-meta.tmpl
  23. +5
    -1
      templates/pad.tmpl
  24. +3
    -0
      templates/password-collection.tmpl
  25. +4
    -1
      templates/post.tmpl
  26. +2
    -0
      templates/user/admin/users.tmpl
  27. +37
    -0
      templates/user/admin/view-user.tmpl
  28. +3
    -0
      templates/user/articles.tmpl
  29. +3
    -0
      templates/user/collection.tmpl
  30. +3
    -0
      templates/user/collections.tmpl
  31. +5
    -0
      templates/user/include/suspended.tmpl
  32. +3
    -0
      templates/user/settings.tmpl
  33. +3
    -0
      templates/user/stats.tmpl
  34. +12
    -0
      users.go
  35. +10
    -1
      webfinger.go

+ 36
- 7
account.go Dosyayı Görüntüle

@@ -750,14 +750,20 @@ func viewArticles(app *App, u *User, w http.ResponseWriter, r *http.Request) err
log.Error("unable to fetch collections: %v", err) log.Error("unable to fetch collections: %v", err)
} }


suspended, err := app.db.IsUserSuspended(u.ID)
if err != nil {
log.Error("view articles: %v", err)
}
d := struct { d := struct {
*UserPage *UserPage
AnonymousPosts *[]PublicPost AnonymousPosts *[]PublicPost
Collections *[]Collection Collections *[]Collection
Suspended bool
}{ }{
UserPage: NewUserPage(app, r, u, u.Username+"'s Posts", f), UserPage: NewUserPage(app, r, u, u.Username+"'s Posts", f),
AnonymousPosts: p, AnonymousPosts: p,
Collections: c, Collections: c,
Suspended: suspended,
} }
d.UserPage.SetMessaging(u) d.UserPage.SetMessaging(u)
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
@@ -779,6 +785,11 @@ func viewCollections(app *App, u *User, w http.ResponseWriter, r *http.Request)
uc, _ := app.db.GetUserCollectionCount(u.ID) uc, _ := app.db.GetUserCollectionCount(u.ID)
// TODO: handle any errors // TODO: handle any errors


suspended, err := app.db.IsUserSuspended(u.ID)
if err != nil {
log.Error("view collections %v", err)
return fmt.Errorf("view collections: %v", err)
}
d := struct { d := struct {
*UserPage *UserPage
Collections *[]Collection Collections *[]Collection
@@ -786,11 +797,13 @@ func viewCollections(app *App, u *User, w http.ResponseWriter, r *http.Request)
UsedCollections, TotalCollections int UsedCollections, TotalCollections int


NewBlogsDisabled bool NewBlogsDisabled bool
Suspended bool
}{ }{
UserPage: NewUserPage(app, r, u, u.Username+"'s Blogs", f), UserPage: NewUserPage(app, r, u, u.Username+"'s Blogs", f),
Collections: c, Collections: c,
UsedCollections: int(uc), UsedCollections: int(uc),
NewBlogsDisabled: !app.cfg.App.CanCreateBlogs(uc), NewBlogsDisabled: !app.cfg.App.CanCreateBlogs(uc),
Suspended: suspended,
} }
d.UserPage.SetMessaging(u) d.UserPage.SetMessaging(u)
showUserPage(w, "collections", d) showUserPage(w, "collections", d)
@@ -808,13 +821,20 @@ func viewEditCollection(app *App, u *User, w http.ResponseWriter, r *http.Reques
return ErrCollectionNotFound return ErrCollectionNotFound
} }


suspended, err := app.db.IsUserSuspended(u.ID)
if err != nil {
log.Error("view edit collection %v", err)
return fmt.Errorf("view edit collection: %v", err)
}
flashes, _ := getSessionFlashes(app, w, r, nil) flashes, _ := getSessionFlashes(app, w, r, nil)
obj := struct { obj := struct {
*UserPage *UserPage
*Collection *Collection
Suspended bool
}{ }{
UserPage: NewUserPage(app, r, u, "Edit "+c.DisplayTitle(), flashes), UserPage: NewUserPage(app, r, u, "Edit "+c.DisplayTitle(), flashes),
Collection: c, Collection: c,
Suspended: suspended,
} }


showUserPage(w, "collection", obj) showUserPage(w, "collection", obj)
@@ -976,17 +996,24 @@ func viewStats(app *App, u *User, w http.ResponseWriter, r *http.Request) error
titleStats = c.DisplayTitle() + " " titleStats = c.DisplayTitle() + " "
} }


suspended, err := app.db.IsUserSuspended(u.ID)
if err != nil {
log.Error("view stats: %v", err)
return err
}
obj := struct { obj := struct {
*UserPage *UserPage
VisitsBlog string VisitsBlog string
Collection *Collection Collection *Collection
TopPosts *[]PublicPost TopPosts *[]PublicPost
APFollowers int APFollowers int
Suspended bool
}{ }{
UserPage: NewUserPage(app, r, u, titleStats+"Stats", flashes), UserPage: NewUserPage(app, r, u, titleStats+"Stats", flashes),
VisitsBlog: alias, VisitsBlog: alias,
Collection: c, Collection: c,
TopPosts: topPosts, TopPosts: topPosts,
Suspended: suspended,
} }
if app.cfg.App.Federation { if app.cfg.App.Federation {
folls, err := app.db.GetAPFollowers(c) folls, err := app.db.GetAPFollowers(c)
@@ -1017,14 +1044,16 @@ func viewSettings(app *App, u *User, w http.ResponseWriter, r *http.Request) err


obj := struct { obj := struct {
*UserPage *UserPage
Email string
HasPass bool
IsLogOut bool
Email string
HasPass bool
IsLogOut bool
Suspended bool
}{ }{
UserPage: NewUserPage(app, r, u, "Account Settings", flashes),
Email: fullUser.EmailClear(app.keys),
HasPass: passIsSet,
IsLogOut: r.FormValue("logout") == "1",
UserPage: NewUserPage(app, r, u, "Account Settings", flashes),
Email: fullUser.EmailClear(app.keys),
HasPass: passIsSet,
IsLogOut: r.FormValue("logout") == "1",
Suspended: fullUser.IsSilenced(),
} }


showUserPage(w, "settings", obj) showUserPage(w, "settings", obj)


+ 40
- 0
activitypub.go Dosyayı Görüntüle

@@ -80,6 +80,14 @@ func handleFetchCollectionActivities(app *App, w http.ResponseWriter, r *http.Re
if err != nil { if err != nil {
return err return err
} }
suspended, err := app.db.IsUserSuspended(c.OwnerID)
if err != nil {
log.Error("fetch collection activities: %v", err)
return ErrInternalGeneral
}
if suspended {
return ErrCollectionNotFound
}
c.hostName = app.cfg.App.Host c.hostName = app.cfg.App.Host


p := c.PersonObject() p := c.PersonObject()
@@ -105,6 +113,14 @@ func handleFetchCollectionOutbox(app *App, w http.ResponseWriter, r *http.Reques
if err != nil { if err != nil {
return err return err
} }
suspended, err := app.db.IsUserSuspended(c.OwnerID)
if err != nil {
log.Error("fetch collection outbox: %v", err)
return ErrInternalGeneral
}
if suspended {
return ErrCollectionNotFound
}
c.hostName = app.cfg.App.Host c.hostName = app.cfg.App.Host


if app.cfg.App.SingleUser { if app.cfg.App.SingleUser {
@@ -158,6 +174,14 @@ func handleFetchCollectionFollowers(app *App, w http.ResponseWriter, r *http.Req
if err != nil { if err != nil {
return err return err
} }
suspended, err := app.db.IsUserSuspended(c.OwnerID)
if err != nil {
log.Error("fetch collection followers: %v", err)
return ErrInternalGeneral
}
if suspended {
return ErrCollectionNotFound
}
c.hostName = app.cfg.App.Host c.hostName = app.cfg.App.Host


accountRoot := c.FederatedAccount() accountRoot := c.FederatedAccount()
@@ -204,6 +228,14 @@ func handleFetchCollectionFollowing(app *App, w http.ResponseWriter, r *http.Req
if err != nil { if err != nil {
return err return err
} }
suspended, err := app.db.IsUserSuspended(c.OwnerID)
if err != nil {
log.Error("fetch collection following: %v", err)
return ErrInternalGeneral
}
if suspended {
return ErrCollectionNotFound
}
c.hostName = app.cfg.App.Host c.hostName = app.cfg.App.Host


accountRoot := c.FederatedAccount() accountRoot := c.FederatedAccount()
@@ -238,6 +270,14 @@ func handleFetchCollectionInbox(app *App, w http.ResponseWriter, r *http.Request
// TODO: return Reject? // TODO: return Reject?
return err return err
} }
suspended, err := app.db.IsUserSuspended(c.OwnerID)
if err != nil {
log.Error("fetch collection inbox: %v", err)
return ErrInternalGeneral
}
if suspended {
return ErrCollectionNotFound
}
c.hostName = app.cfg.App.Host c.hostName = app.cfg.App.Host


if debugging { if debugging {


+ 25
- 0
admin.go Dosyayı Görüntüle

@@ -241,12 +241,37 @@ func handleViewAdminUser(app *App, u *User, w http.ResponseWriter, r *http.Reque
return nil 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 { func handleAdminResetUserPass(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r) vars := mux.Vars(r)
username := vars["username"] username := vars["username"]
if username == "" { if username == "" {
return impart.HTTPError{http.StatusFound, "/admin/users"} return impart.HTTPError{http.StatusFound, "/admin/users"}
} }

// Generate new random password since none supplied // Generate new random password since none supplied
pass := passgen.NewWordish() pass := passgen.NewWordish()
hashedPass, err := auth.HashPass([]byte(pass)) hashedPass, err := auth.HashPass([]byte(pass))


+ 36
- 4
collections.go Dosyayı Görüntüle

@@ -71,6 +71,7 @@ type (
CurrentPage int CurrentPage int
TotalPages int TotalPages int
Format *CollectionFormat Format *CollectionFormat
Suspended bool
} }
SubmittedCollection struct { SubmittedCollection struct {
// Data used for updating a given collection // Data used for updating a given collection
@@ -379,6 +380,7 @@ func newCollection(app *App, w http.ResponseWriter, r *http.Request) error {
} }


var userID int64 var userID int64
var err error
if reqJSON && !c.Web { if reqJSON && !c.Web {
accessToken = r.Header.Get("Authorization") accessToken = r.Header.Get("Authorization")
if accessToken == "" { if accessToken == "" {
@@ -395,6 +397,14 @@ func newCollection(app *App, w http.ResponseWriter, r *http.Request) error {
} }
userID = u.ID userID = u.ID
} }
suspended, err := app.db.IsUserSuspended(userID)
if err != nil {
log.Error("new collection: %v", err)
return ErrInternalGeneral
}
if suspended {
return ErrUserSuspended
}


if !author.IsValidUsername(app.cfg, c.Alias) { if !author.IsValidUsername(app.cfg, c.Alias) {
return impart.HTTPError{http.StatusPreconditionFailed, "Collection alias isn't valid."} return impart.HTTPError{http.StatusPreconditionFailed, "Collection alias isn't valid."}
@@ -477,6 +487,7 @@ func fetchCollection(app *App, w http.ResponseWriter, r *http.Request) error {
res.Owner = u res.Owner = u
} }
} }
// TODO: check suspended
app.db.GetPostsCount(res, isCollOwner) app.db.GetPostsCount(res, isCollOwner)
// Strip non-public information // Strip non-public information
res.Collection.ForPublic() res.Collection.ForPublic()
@@ -725,9 +736,14 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro
if c == nil || err != nil { if c == nil || err != nil {
return err return err
} }

c.hostName = app.cfg.App.Host c.hostName = app.cfg.App.Host


suspended, err := app.db.IsUserSuspended(c.OwnerID)
if err != nil {
log.Error("view collection: %v", err)
return ErrInternalGeneral
}

// Serve ActivityStreams data now, if requested // Serve ActivityStreams data now, if requested
if strings.Contains(r.Header.Get("Accept"), "application/activity+json") { if strings.Contains(r.Header.Get("Accept"), "application/activity+json") {
ac := c.PersonObject() ac := c.PersonObject()
@@ -784,6 +800,10 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro
log.Error("Error getting user for collection: %v", err) log.Error("Error getting user for collection: %v", err)
} }
} }
if !isOwner && suspended {
return ErrCollectionNotFound
}
displayPage.Suspended = isOwner && suspended
displayPage.Owner = owner displayPage.Owner = owner
coll.Owner = displayPage.Owner coll.Owner = displayPage.Owner


@@ -886,6 +906,10 @@ func handleViewCollectionTag(app *App, w http.ResponseWriter, r *http.Request) e
log.Error("Error getting user for collection: %v", err) log.Error("Error getting user for collection: %v", err)
} }
} }
if !isOwner && u.IsSilenced() {
return ErrCollectionNotFound
}
displayPage.Suspended = u.IsSilenced()
displayPage.Owner = owner displayPage.Owner = owner
coll.Owner = displayPage.Owner coll.Owner = displayPage.Owner
// Add more data // Add more data
@@ -924,11 +948,10 @@ func existingCollection(app *App, w http.ResponseWriter, r *http.Request) error
collAlias := vars["alias"] collAlias := vars["alias"]
isWeb := r.FormValue("web") == "1" isWeb := r.FormValue("web") == "1"


var u *User
u := &User{}
if reqJSON && !isWeb { if reqJSON && !isWeb {
// Ensure an access token was given // Ensure an access token was given
accessToken := r.Header.Get("Authorization") accessToken := r.Header.Get("Authorization")
u = &User{}
u.ID = app.db.GetUserID(accessToken) u.ID = app.db.GetUserID(accessToken)
if u.ID == -1 { if u.ID == -1 {
return ErrBadAccessToken return ErrBadAccessToken
@@ -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" { if r.Method == "DELETE" {
err := app.db.DeleteCollection(collAlias, u.ID) err := app.db.DeleteCollection(collAlias, u.ID)
if err != nil { if err != nil {
@@ -952,7 +985,6 @@ func existingCollection(app *App, w http.ResponseWriter, r *http.Request) error
} }


c := SubmittedCollection{OwnerID: uint64(u.ID)} c := SubmittedCollection{OwnerID: uint64(u.ID)}
var err error


if reqJSON { if reqJSON {
// Decode JSON request // Decode JSON request


+ 43
- 9
database.go Dosyayı Görüntüle

@@ -296,7 +296,7 @@ func (db *datastore) CreateCollection(cfg *config.Config, alias, title string, u
func (db *datastore) GetUserByID(id int64) (*User, error) { func (db *datastore) GetUserByID(id int64) (*User, error) {
u := &User{ID: id} u := &User{ID: id}


err := db.QueryRow("SELECT username, password, email, created FROM users WHERE id = ?", id).Scan(&u.Username, &u.HashedPass, &u.Email, &u.Created)
err := db.QueryRow("SELECT username, password, email, created, status FROM users WHERE id = ?", id).Scan(&u.Username, &u.HashedPass, &u.Email, &u.Created, &u.Status)
switch { switch {
case err == sql.ErrNoRows: case err == sql.ErrNoRows:
return nil, ErrUserNotFound return nil, ErrUserNotFound
@@ -308,6 +308,23 @@ func (db *datastore) GetUserByID(id int64) (*User, error) {
return u, nil return u, nil
} }


// IsUserSuspended returns true if the user account associated with id is
// currently suspended.
func (db *datastore) IsUserSuspended(id int64) (bool, error) {
u := &User{ID: id}

err := db.QueryRow("SELECT 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 // DoesUserNeedAuth returns true if the user hasn't provided any methods for
// authenticating with the account, such a passphrase or email address. // authenticating with the account, such a passphrase or email address.
// Any errors are reported to admin and silently quashed, returning false as the // Any errors are reported to admin and silently quashed, returning false as the
@@ -347,7 +364,7 @@ func (db *datastore) IsUserPassSet(id int64) (bool, error) {
func (db *datastore) GetUserForAuth(username string) (*User, error) { func (db *datastore) GetUserForAuth(username string) (*User, error) {
u := &User{Username: username} u := &User{Username: username}


err := db.QueryRow("SELECT id, password, email, created FROM users WHERE username = ?", username).Scan(&u.ID, &u.HashedPass, &u.Email, &u.Created)
err := db.QueryRow("SELECT id, password, email, created, status FROM users WHERE username = ?", username).Scan(&u.ID, &u.HashedPass, &u.Email, &u.Created, &u.Status)
switch { switch {
case err == sql.ErrNoRows: case err == sql.ErrNoRows:
// Check if they've entered the wrong, unnormalized username // Check if they've entered the wrong, unnormalized username
@@ -370,7 +387,7 @@ func (db *datastore) GetUserForAuth(username string) (*User, error) {
func (db *datastore) GetUserForAuthByID(userID int64) (*User, error) { func (db *datastore) GetUserForAuthByID(userID int64) (*User, error) {
u := &User{ID: userID} u := &User{ID: userID}


err := db.QueryRow("SELECT id, password, email, created FROM users WHERE id = ?", u.ID).Scan(&u.ID, &u.HashedPass, &u.Email, &u.Created)
err := db.QueryRow("SELECT id, password, email, created, status FROM users WHERE id = ?", u.ID).Scan(&u.ID, &u.HashedPass, &u.Email, &u.Created, &u.Status)
switch { switch {
case err == sql.ErrNoRows: case err == sql.ErrNoRows:
return nil, ErrUserNotFound return nil, ErrUserNotFound
@@ -1629,7 +1646,11 @@ func (db *datastore) GetMeStats(u *User) userMeStats {
} }


func (db *datastore) GetTotalCollections() (collCount int64, err error) { func (db *datastore) GetTotalCollections() (collCount int64, err error) {
err = db.QueryRow(`SELECT COUNT(*) FROM collections`).Scan(&collCount)
err = db.QueryRow(`
SELECT COUNT(*)
FROM collections c
LEFT JOIN users u ON u.id = c.owner_id
WHERE u.status = 0`).Scan(&collCount)
if err != nil { if err != nil {
log.Error("Unable to fetch collections count: %v", err) log.Error("Unable to fetch collections count: %v", err)
} }
@@ -1637,7 +1658,11 @@ func (db *datastore) GetTotalCollections() (collCount int64, err error) {
} }


func (db *datastore) GetTotalPosts() (postCount int64, err error) { func (db *datastore) GetTotalPosts() (postCount int64, err error) {
err = db.QueryRow(`SELECT COUNT(*) FROM posts`).Scan(&postCount)
err = db.QueryRow(`
SELECT COUNT(*)
FROM posts p
LEFT JOIN users u ON u.id = p.owner_id
WHERE u.status = 0`).Scan(&postCount)
if err != nil { if err != nil {
log.Error("Unable to fetch posts count: %v", err) log.Error("Unable to fetch posts count: %v", err)
} }
@@ -2359,17 +2384,17 @@ func (db *datastore) GetAllUsers(page uint) (*[]User, error) {
limitStr = fmt.Sprintf("%d, %d", (page-1)*adminUsersPerPage, adminUsersPerPage) limitStr = fmt.Sprintf("%d, %d", (page-1)*adminUsersPerPage, adminUsersPerPage)
} }


rows, err := db.Query("SELECT id, username, created FROM users ORDER BY created DESC LIMIT " + limitStr)
rows, err := db.Query("SELECT id, username, created, status FROM users ORDER BY created DESC LIMIT " + limitStr)
if err != nil { if err != nil {
log.Error("Failed selecting from posts: %v", err)
return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve user posts."}
log.Error("Failed selecting from users: %v", err)
return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve all users."}
} }
defer rows.Close() defer rows.Close()


users := []User{} users := []User{}
for rows.Next() { for rows.Next() {
u := User{} u := User{}
err = rows.Scan(&u.ID, &u.Username, &u.Created)
err = rows.Scan(&u.ID, &u.Username, &u.Created, &u.Status)
if err != nil { if err != nil {
log.Error("Failed scanning GetAllUsers() row: %v", err) log.Error("Failed scanning GetAllUsers() row: %v", err)
break break
@@ -2406,6 +2431,15 @@ func (db *datastore) GetUserLastPostTime(id int64) (*time.Time, error) {
return &t, nil 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) { func (db *datastore) GetCollectionLastPostTime(id int64) (*time.Time, error) {
var t time.Time var t time.Time
err := db.QueryRow("SELECT created FROM posts WHERE collection_id = ? ORDER BY created DESC LIMIT 1", id).Scan(&t) err := db.QueryRow("SELECT created FROM posts WHERE collection_id = ? ORDER BY created DESC LIMIT 1", id).Scan(&t)


+ 4
- 1
errors.go Dosyayı Görüntüle

@@ -11,8 +11,9 @@
package writefreely package writefreely


import ( import (
"github.com/writeas/impart"
"net/http" "net/http"

"github.com/writeas/impart"
) )


// Commonly returned HTTP errors // Commonly returned HTTP errors
@@ -46,6 +47,8 @@ var (


ErrUserNotFound = impart.HTTPError{http.StatusNotFound, "User doesn't exist."} ErrUserNotFound = impart.HTTPError{http.StatusNotFound, "User doesn't exist."}
ErrUserNotFoundEmail = impart.HTTPError{http.StatusNotFound, "Please enter your username instead of your email address."} ErrUserNotFoundEmail = impart.HTTPError{http.StatusNotFound, "Please enter your username instead of your email address."}

ErrUserSuspended = impart.HTTPError{http.StatusForbidden, "Account is silenced."}
) )


// Post operation errors // Post operation errors


+ 12
- 2
feed.go Dosyayı Görüntüle

@@ -12,12 +12,13 @@ package writefreely


import ( import (
"fmt" "fmt"
"net/http"
"time"

. "github.com/gorilla/feeds" . "github.com/gorilla/feeds"
"github.com/gorilla/mux" "github.com/gorilla/mux"
stripmd "github.com/writeas/go-strip-markdown" stripmd "github.com/writeas/go-strip-markdown"
"github.com/writeas/web-core/log" "github.com/writeas/web-core/log"
"net/http"
"time"
) )


func ViewFeed(app *App, w http.ResponseWriter, req *http.Request) error { func ViewFeed(app *App, w http.ResponseWriter, req *http.Request) error {
@@ -34,6 +35,15 @@ func ViewFeed(app *App, w http.ResponseWriter, req *http.Request) error {
if err != nil { if err != nil {
return nil return nil
} }

suspended, err := app.db.IsUserSuspended(c.OwnerID)
if err != nil {
log.Error("view feed: get user: %v", err)
return ErrInternalGeneral
}
if suspended {
return ErrCollectionNotFound
}
c.hostName = app.cfg.App.Host c.hostName = app.cfg.App.Host


if c.IsPrivate() || c.IsProtected() { if c.IsPrivate() || c.IsProtected() {


+ 4
- 0
invites.go Dosyayı Görüntüle

@@ -78,6 +78,10 @@ func handleCreateUserInvite(app *App, u *User, w http.ResponseWriter, r *http.Re
muVal := r.FormValue("uses") muVal := r.FormValue("uses")
expVal := r.FormValue("expires") expVal := r.FormValue("expires")


if u.IsSilenced() {
return ErrUserSuspended
}

var err error var err error
var maxUses int var maxUses int
if muVal != "0" { if muVal != "0" {


+ 8
- 1
less/core.less Dosyayı Görüntüle

@@ -516,10 +516,17 @@ abbr {
body#collection article p, body#subpage article p { body#collection article p, body#subpage article p {
.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; max-width: 40rem;
margin: 0 auto; 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 { textarea, pre, body#post article, body#collection article p {
&.norm, &.sans, &.wrap { &.norm, &.sans, &.wrap {
line-height: 1.4em; line-height: 1.4em;


+ 2
- 0
migrations/migrations.go Dosyayı Görüntüle

@@ -13,6 +13,7 @@ package migrations


import ( import (
"database/sql" "database/sql"

"github.com/writeas/web-core/log" "github.com/writeas/web-core/log"
) )


@@ -57,6 +58,7 @@ func (m *migration) Migrate(db *datastore) error {
var migrations = []Migration{ var migrations = []Migration{
New("support user invites", supportUserInvites), // -> V1 (v0.8.0) New("support user invites", supportUserInvites), // -> V1 (v0.8.0)
New("support dynamic instance pages", supportInstancePages), // V1 -> V2 (v0.9.0) New("support dynamic instance pages", supportInstancePages), // V1 -> V2 (v0.9.0)
New("support users suspension", supportUserStatus), // V2 -> V3 (v0.11.0)
} }


// CurrentVer returns the current migration version the application is on // CurrentVer returns the current migration version the application is on


+ 29
- 0
migrations/v3.go Dosyayı Görüntüle

@@ -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
}

+ 14
- 3
pad.go Dosyayı Görüntüle

@@ -35,9 +35,10 @@ func handleViewPad(app *App, w http.ResponseWriter, r *http.Request) error {
} }
appData := &struct { appData := &struct {
page.StaticPage page.StaticPage
Post *RawPost
User *User
Blogs *[]Collection
Post *RawPost
User *User
Blogs *[]Collection
Suspended bool


Editing bool // True if we're modifying an existing post Editing bool // True if we're modifying an existing post
EditCollection *Collection // Collection of the post we're editing, if any EditCollection *Collection // Collection of the post we're editing, if any
@@ -52,6 +53,10 @@ func handleViewPad(app *App, w http.ResponseWriter, r *http.Request) error {
if err != nil { if err != nil {
log.Error("Unable to get user's blogs for Pad: %v", err) log.Error("Unable to get user's blogs for Pad: %v", err)
} }
appData.Suspended, err = app.db.IsUserSuspended(appData.User.ID)
if err != nil {
log.Error("Unable to get users suspension status for Pad: %v", err)
}
} }


padTmpl := app.cfg.App.Editor padTmpl := app.cfg.App.Editor
@@ -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 EditCollection *Collection // Collection of the post we're editing, if any
Flashes []string Flashes []string
NeedsToken bool NeedsToken bool
Suspended bool
}{ }{
StaticPage: pageForReq(app, r), StaticPage: pageForReq(app, r),
Post: &RawPost{Font: "norm"}, Post: &RawPost{Font: "norm"},
User: getUserSession(app, r), User: getUserSession(app, r),
} }
var err error var err error
appData.Suspended, err = app.db.IsUserSuspended(appData.User.ID)
if err != nil {
log.Error("view meta: get user suspended status: %v", err)
return ErrInternalGeneral
}


if action == "" && slug == "" { if action == "" && slug == "" {
return ErrPostNotFound return ErrPostNotFound


+ 76
- 8
posts.go Dosyayı Görüntüle

@@ -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 // Check if post has been unpublished
if content == "" { if content == "" {
gone = true gone = true
@@ -428,9 +434,10 @@ func handleViewPost(app *App, w http.ResponseWriter, r *http.Request) error {
page := struct { page := struct {
*AnonymousPost *AnonymousPost
page.StaticPage page.StaticPage
Username string
IsOwner bool
SiteURL string
Username string
IsOwner bool
SiteURL string
Suspended bool
}{ }{
AnonymousPost: post, AnonymousPost: post,
StaticPage: pageForReq(app, r), 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 page.IsOwner = ownerID.Valid && ownerID.Int64 == u.ID
} }


if !page.IsOwner && suspended {
return ErrPostNotFound
}
page.Suspended = suspended
err = templates["post"].ExecuteTemplate(w, "post", page) err = templates["post"].ExecuteTemplate(w, "post", page)
if err != nil { if err != nil {
log.Error("Post template execute error: %v", err) log.Error("Post template execute error: %v", err)
@@ -497,6 +508,15 @@ func newPost(app *App, w http.ResponseWriter, r *http.Request) error {
} else { } else {
userID = app.db.GetUserID(accessToken) userID = app.db.GetUserID(accessToken)
} }
suspended, err := app.db.IsUserSuspended(userID)
if err != nil {
log.Error("new post: %v", err)
return ErrInternalGeneral
}
if suspended {
return ErrUserSuspended
}

if userID == -1 { if userID == -1 {
return ErrNotLoggedIn return ErrNotLoggedIn
} }
@@ -509,7 +529,7 @@ func newPost(app *App, w http.ResponseWriter, r *http.Request) error {
var p *SubmittedPost var p *SubmittedPost
if reqJSON { if reqJSON {
decoder := json.NewDecoder(r.Body) decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&p)
err = decoder.Decode(&p)
if err != nil { if err != nil {
log.Error("Couldn't parse new post JSON request: %v\n", err) log.Error("Couldn't parse new post JSON request: %v\n", err)
return ErrBadJSON return ErrBadJSON
@@ -555,7 +575,6 @@ func newPost(app *App, w http.ResponseWriter, r *http.Request) error {


var newPost *PublicPost = &PublicPost{} var newPost *PublicPost = &PublicPost{}
var coll *Collection var coll *Collection
var err error
if accessToken != "" { if accessToken != "" {
newPost, err = app.db.CreateOwnedPost(p, accessToken, collAlias, app.cfg.App.Host) newPost, err = app.db.CreateOwnedPost(p, accessToken, collAlias, app.cfg.App.Host)
} else { } else {
@@ -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 // Modify post struct
p.ID = postID p.ID = postID


@@ -857,11 +885,20 @@ func addPost(app *App, w http.ResponseWriter, r *http.Request) error {
ownerID = u.ID ownerID = u.ID
} }


suspended, err := app.db.IsUserSuspended(ownerID)
if err != nil {
log.Error("add post: %v", err)
return ErrInternalGeneral
}
if suspended {
return ErrUserSuspended
}

// Parse claimed posts in format: // Parse claimed posts in format:
// [{"id": "...", "token": "..."}] // [{"id": "...", "token": "..."}]
var claims *[]ClaimPostRequest var claims *[]ClaimPostRequest
decoder := json.NewDecoder(r.Body) decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&claims)
err = decoder.Decode(&claims)
if err != nil { if err != nil {
return ErrBadJSONArray return ErrBadJSONArray
} }
@@ -951,13 +988,22 @@ func pinPost(app *App, w http.ResponseWriter, r *http.Request) error {
userID = u.ID userID = u.ID
} }


suspended, err := app.db.IsUserSuspended(userID)
if err != nil {
log.Error("pin post: %v", err)
return ErrInternalGeneral
}
if suspended {
return ErrUserSuspended
}

// Parse request // Parse request
var posts []struct { var posts []struct {
ID string `json:"id"` ID string `json:"id"`
Position int64 `json:"position"` Position int64 `json:"position"`
} }
decoder := json.NewDecoder(r.Body) decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&posts)
err = decoder.Decode(&posts)
if err != nil { if err != nil {
return ErrBadJSONArray return ErrBadJSONArray
} }
@@ -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 { func fetchPost(app *App, w http.ResponseWriter, r *http.Request) error {
var collID int64 var collID int64
var ownerID int64
var coll *Collection var coll *Collection
var err error var err error
vars := mux.Vars(r) vars := mux.Vars(r)
@@ -1008,12 +1055,22 @@ func fetchPost(app *App, w http.ResponseWriter, r *http.Request) error {
return err return err
} }
collID = coll.ID collID = coll.ID
ownerID = coll.OwnerID
} }


p, err := app.db.GetPost(vars["post"], collID) p, err := app.db.GetPost(vars["post"], collID)
if err != nil { if err != nil {
return err return err
} }
suspended, err := app.db.IsUserSuspended(ownerID)
if err != nil {
log.Error("fetch post: %v", err)
return ErrInternalGeneral
}

if suspended {
return ErrPostNotFound
}


p.extractData() p.extractData()


@@ -1275,6 +1332,12 @@ func viewCollectionPost(app *App, w http.ResponseWriter, r *http.Request) error
} }
c.hostName = app.cfg.App.Host c.hostName = app.cfg.App.Host


suspended, err := app.db.IsUserSuspended(c.OwnerID)
if err != nil {
log.Error("view collection post: %v", err)
return ErrInternalGeneral
}

// Check collection permissions // Check collection permissions
if c.IsPrivate() && (u == nil || u.ID != c.OwnerID) { if c.IsPrivate() && (u == nil || u.ID != c.OwnerID) {
return ErrPostNotFound return ErrPostNotFound
@@ -1327,10 +1390,13 @@ Are you sure it was ever here?`,
return err 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.Collection = coll
p.IsTopLevel = app.cfg.App.SingleUser p.IsTopLevel = app.cfg.App.SingleUser


if !p.IsOwner && suspended {
return ErrPostNotFound
}
// Check if post has been unpublished // Check if post has been unpublished
if p.Content == "" && p.Title.String == "" { if p.Content == "" && p.Title.String == "" {
return impart.HTTPError{http.StatusGone, "Post was unpublished."} return impart.HTTPError{http.StatusGone, "Post was unpublished."}
@@ -1380,12 +1446,14 @@ Are you sure it was ever here?`,
IsFound bool IsFound bool
IsAdmin bool IsAdmin bool
CanInvite bool CanInvite bool
Suspended bool
}{ }{
PublicPost: p, PublicPost: p,
StaticPage: pageForReq(app, r), StaticPage: pageForReq(app, r),
IsOwner: cr.isCollOwner, IsOwner: cr.isCollOwner,
IsCustomDomain: cr.isCustomDomain, IsCustomDomain: cr.isCustomDomain,
IsFound: postFound, IsFound: postFound,
Suspended: suspended,
} }
tp.IsAdmin = u != nil && u.IsAdmin() tp.IsAdmin = u != nil && u.IsAdmin()
tp.CanInvite = canUserInvite(app.cfg, tp.IsAdmin) tp.CanInvite = canUserInvite(app.cfg, tp.IsAdmin)


+ 8
- 6
read.go Dosyayı Görüntüle

@@ -13,6 +13,12 @@ package writefreely
import ( import (
"database/sql" "database/sql"
"fmt" "fmt"
"html/template"
"math"
"net/http"
"strconv"
"time"

. "github.com/gorilla/feeds" . "github.com/gorilla/feeds"
"github.com/gorilla/mux" "github.com/gorilla/mux"
stripmd "github.com/writeas/go-strip-markdown" stripmd "github.com/writeas/go-strip-markdown"
@@ -20,11 +26,6 @@ import (
"github.com/writeas/web-core/log" "github.com/writeas/web-core/log"
"github.com/writeas/web-core/memo" "github.com/writeas/web-core/memo"
"github.com/writeas/writefreely/page" "github.com/writeas/writefreely/page"
"html/template"
"math"
"net/http"
"strconv"
"time"
) )


const ( const (
@@ -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 rows, err := app.db.Query(`SELECT p.id, alias, c.title, p.slug, p.title, p.content, p.text_appearance, p.language, p.rtl, p.created, p.updated
FROM collections c FROM collections c
LEFT JOIN posts p ON p.collection_id = c.id LEFT JOIN posts p ON p.collection_id = c.id
WHERE c.privacy = 1 AND (p.created >= ` + app.db.dateSub(3, "month") + ` AND p.created <= ` + app.db.now() + ` AND pinned_position IS NULL)
LEFT JOIN users u ON u.id = p.owner_id
WHERE c.privacy = 1 AND (p.created >= ` + app.db.dateSub(3, "month") + ` AND p.created <= ` + app.db.now() + ` AND pinned_position IS NULL) AND u.status = 0
ORDER BY p.created DESC`) ORDER BY p.created DESC`)
if err != nil { if err != nil {
log.Error("Failed selecting from posts: %v", err) log.Error("Failed selecting from posts: %v", err)


+ 1
- 0
routes.go Dosyayı Görüntüle

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


+ 8
- 4
templates.go Dosyayı Görüntüle

@@ -11,10 +11,6 @@
package writefreely package writefreely


import ( import (
"github.com/dustin/go-humanize"
"github.com/writeas/web-core/l10n"
"github.com/writeas/web-core/log"
"github.com/writeas/writefreely/config"
"html/template" "html/template"
"io" "io"
"io/ioutil" "io/ioutil"
@@ -22,6 +18,11 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"

"github.com/dustin/go-humanize"
"github.com/writeas/web-core/l10n"
"github.com/writeas/web-core/log"
"github.com/writeas/writefreely/config"
) )


var ( var (
@@ -63,6 +64,7 @@ func initTemplate(parentDir, name string) {
filepath.Join(parentDir, templatesDir, name+".tmpl"), filepath.Join(parentDir, templatesDir, name+".tmpl"),
filepath.Join(parentDir, templatesDir, "include", "footer.tmpl"), filepath.Join(parentDir, templatesDir, "include", "footer.tmpl"),
filepath.Join(parentDir, templatesDir, "base.tmpl"), filepath.Join(parentDir, templatesDir, "base.tmpl"),
filepath.Join(parentDir, templatesDir, "user", "include", "suspended.tmpl"),
} }
if name == "collection" || name == "collection-tags" || name == "chorus-collection" { if name == "collection" || name == "collection-tags" || name == "chorus-collection" {
// These pages list out collection posts, so we also parse templatesDir + "include/posts.tmpl" // These pages list out collection posts, so we also parse templatesDir + "include/posts.tmpl"
@@ -86,6 +88,7 @@ func initPage(parentDir, path, key string) {
path, path,
filepath.Join(parentDir, templatesDir, "include", "footer.tmpl"), filepath.Join(parentDir, templatesDir, "include", "footer.tmpl"),
filepath.Join(parentDir, templatesDir, "base.tmpl"), filepath.Join(parentDir, templatesDir, "base.tmpl"),
filepath.Join(parentDir, templatesDir, "user", "include", "suspended.tmpl"),
)) ))
} }


@@ -98,6 +101,7 @@ func initUserPage(parentDir, path, key string) {
path, path,
filepath.Join(parentDir, templatesDir, "user", "include", "header.tmpl"), filepath.Join(parentDir, templatesDir, "user", "include", "header.tmpl"),
filepath.Join(parentDir, templatesDir, "user", "include", "footer.tmpl"), filepath.Join(parentDir, templatesDir, "user", "include", "footer.tmpl"),
filepath.Join(parentDir, templatesDir, "user", "include", "suspended.tmpl"),
)) ))
} }




+ 3
- 0
templates/chorus-collection-post.tmpl Dosyayı Görüntüle

@@ -65,6 +65,9 @@ article time.dt-published {


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


{{ if .Collection.ShowFooterBranding }} {{ if .Collection.ShowFooterBranding }}


+ 3
- 0
templates/chorus-collection.tmpl Dosyayı Görüntüle

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


+ 3
- 0
templates/collection-post.tmpl Dosyayı Görüntüle

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


{{ if .Collection.ShowFooterBranding }} {{ if .Collection.ShowFooterBranding }}


+ 3
- 0
templates/collection-tags.tmpl Dosyayı Görüntüle

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


+ 3
- 0
templates/collection.tmpl Dosyayı Görüntüle

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


+ 4
- 0
templates/edit-meta.tmpl Dosyayı Görüntüle

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


+ 5
- 1
templates/pad.tmpl Dosyayı Görüntüle

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


+ 3
- 0
templates/password-collection.tmpl Dosyayı Görüntüle

@@ -25,6 +25,9 @@


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


+ 4
- 1
templates/post.tmpl Dosyayı Görüntüle

@@ -35,7 +35,6 @@
{{template "highlighting" .}} {{template "highlighting" .}}
</head> </head>
<body id="post"> <body id="post">

<header> <header>
<h1 dir="{{.Direction}}"><a href="/">{{.SiteName}}</a></h1> <h1 dir="{{.Direction}}"><a href="/">{{.SiteName}}</a></h1>
<nav> <nav>
@@ -49,6 +48,10 @@
{{ end }} {{ end }}
</nav> </nav>
</header> </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> <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>




+ 2
- 0
templates/user/admin/users.tmpl Dosyayı Görüntüle

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


+ 37
- 0
templates/user/admin/view-user.tmpl Dosyayı Görüntüle

@@ -7,6 +7,24 @@ table.classy th {
h3 { h3 {
font-weight: normal; font-weight: normal;
} }
td.active-suspend {
display: flex;
align-items: center;
}

td.active-suspend > input[type="submit"] {
margin-left: auto;
margin-right: 5%;
}

@media only screen and (max-width: 500px) {
td.active-suspend {
flex-wrap: wrap;
}
td.active-suspend > input[type="submit"] {
margin: auto;
}
}
input.copy-text { input.copy-text {
text-align: center; text-align: center;
font-size: 1.2em; font-size: 1.2em;
@@ -52,6 +70,21 @@ input.copy-text {
<td>{{if .LastPost}}{{.LastPost}}{{else}}Never{{end}}</td> <td>{{if .LastPost}}{{.LastPost}}{{else}}Never{{end}}</td>
</tr> </tr>
<tr> <tr>
<form action="/admin/user/{{.User.Username}}/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> <th>Password</th>
<td> <td>
{{if ne .Username .User.Username}} {{if ne .Username .User.Username}}
@@ -110,6 +143,10 @@ input.copy-text {
</div> </div>


<script type="text/javascript"> <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 = document.getElementById("reset-form");
form.addEventListener('submit', function(e) { form.addEventListener('submit', function(e) {
e.preventDefault(); e.preventDefault();


+ 3
- 0
templates/user/articles.tmpl Dosyayı Görüntüle

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


<h2 id="posts-header">drafts</h2> <h2 id="posts-header">drafts</h2>




+ 3
- 0
templates/user/collection.tmpl Dosyayı Görüntüle

@@ -8,6 +8,9 @@
<div class="content-container snug"> <div class="content-container snug">
<div id="overlay"></div> <div id="overlay"></div>


{{if .Suspended}}
{{template "user-suspended"}}
{{end}}
<h2>Customize {{.DisplayTitle}} <a href="{{if .SingleUser}}/{{else}}/{{.Alias}}/{{end}}">view blog</a></h2> <h2>Customize {{.DisplayTitle}} <a href="{{if .SingleUser}}/{{else}}/{{.Alias}}/{{end}}">view blog</a></h2>


{{if .Flashes}}<ul class="errors"> {{if .Flashes}}<ul class="errors">


+ 3
- 0
templates/user/collections.tmpl Dosyayı Görüntüle

@@ -7,6 +7,9 @@
{{range .Flashes}}<li class="urgent">{{.}}</li>{{end}} {{range .Flashes}}<li class="urgent">{{.}}</li>{{end}}
</ul>{{end}} </ul>{{end}}


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


+ 5
- 0
templates/user/include/suspended.tmpl Dosyayı Görüntüle

@@ -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}}

+ 3
- 0
templates/user/settings.tmpl Dosyayı Görüntüle

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


+ 3
- 0
templates/user/stats.tmpl Dosyayı Görüntüle

@@ -17,6 +17,9 @@ td.none {
</style> </style>


<div class="content-container snug"> <div class="content-container snug">
{{if .Suspended}}
{{template "user-suspended"}}
{{end}}
<h2 id="posts-header">{{if .Collection}}{{.Collection.DisplayTitle}} {{end}}Stats</h2> <h2 id="posts-header">{{if .Collection}}{{.Collection.DisplayTitle}} {{end}}Stats</h2>


<p>Stats for all time.</p> <p>Stats for all time.</p>


+ 12
- 0
users.go Dosyayı Görüntüle

@@ -19,6 +19,13 @@ import (
"github.com/writeas/writefreely/key" "github.com/writeas/writefreely/key"
) )


type UserStatus int

const (
UserActive = iota
UserSilenced
)

type ( type (
userCredentials struct { userCredentials struct {
Alias string `json:"alias" schema:"alias"` Alias string `json:"alias" schema:"alias"`
@@ -59,6 +66,7 @@ type (
HasPass bool `json:"has_pass"` HasPass bool `json:"has_pass"`
Email zero.String `json:"email"` Email zero.String `json:"email"`
Created time.Time `json:"created"` Created time.Time `json:"created"`
Status UserStatus `json:"status"`


clearEmail string `json:"email"` clearEmail string `json:"email"`
} }
@@ -118,3 +126,7 @@ func (u *User) IsAdmin() bool {
// TODO: get this from database // TODO: get this from database
return u.ID == 1 return u.ID == 1
} }

func (u *User) IsSilenced() bool {
return u.Status&UserSilenced != 0
}

+ 10
- 1
webfinger.go Dosyayı Görüntüle

@@ -11,11 +11,12 @@
package writefreely package writefreely


import ( import (
"net/http"

"github.com/writeas/go-webfinger" "github.com/writeas/go-webfinger"
"github.com/writeas/impart" "github.com/writeas/impart"
"github.com/writeas/web-core/log" "github.com/writeas/web-core/log"
"github.com/writeas/writefreely/config" "github.com/writeas/writefreely/config"
"net/http"
) )


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


Yükleniyor…
İptal
Kaydet