mirror of
https://github.com/writefreely/writefreely
synced 2024-11-10 11:24:13 +00:00
Add admin user list
This enables admins on multi-user instances to see all users registered, and view the details of each, including: - Username - Join date - Total posts - Last post date - All blogs - Public info - Views - Total posts - Last post date - Fediverse followers count This is the foundation for future user moderation features. Ref T553
This commit is contained in:
parent
2f4c93cccb
commit
0e722de82c
9 changed files with 312 additions and 0 deletions
101
admin.go
101
admin.go
|
@ -70,6 +70,12 @@ type systemStatus struct {
|
|||
NumGC uint32
|
||||
}
|
||||
|
||||
type inspectedCollection struct {
|
||||
CollectionObj
|
||||
Followers int
|
||||
LastPost string
|
||||
}
|
||||
|
||||
func handleViewAdminDash(app *app, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||
updateAppStats()
|
||||
p := struct {
|
||||
|
@ -104,6 +110,101 @@ func handleViewAdminDash(app *app, u *User, w http.ResponseWriter, r *http.Reque
|
|||
return nil
|
||||
}
|
||||
|
||||
func handleViewAdminUsers(app *app, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||
p := struct {
|
||||
*UserPage
|
||||
Config config.AppCfg
|
||||
Message string
|
||||
|
||||
Users *[]User
|
||||
}{
|
||||
UserPage: NewUserPage(app, r, u, "Users", nil),
|
||||
Config: app.cfg.App,
|
||||
Message: r.FormValue("m"),
|
||||
}
|
||||
|
||||
var err error
|
||||
p.Users, err = app.db.GetAllUsers(1)
|
||||
if err != nil {
|
||||
return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get users: %v", err)}
|
||||
}
|
||||
|
||||
showUserPage(w, "users", p)
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleViewAdminUser(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"}
|
||||
}
|
||||
|
||||
p := struct {
|
||||
*UserPage
|
||||
Config config.AppCfg
|
||||
Message string
|
||||
|
||||
User *User
|
||||
Colls []inspectedCollection
|
||||
LastPost string
|
||||
|
||||
TotalPosts int64
|
||||
}{
|
||||
Config: app.cfg.App,
|
||||
Message: r.FormValue("m"),
|
||||
Colls: []inspectedCollection{},
|
||||
}
|
||||
|
||||
var err error
|
||||
p.User, err = app.db.GetUserForAuth(username)
|
||||
if err != nil {
|
||||
return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get user: %v", err)}
|
||||
}
|
||||
p.UserPage = NewUserPage(app, r, u, p.User.Username, nil)
|
||||
p.TotalPosts = app.db.GetUserPostsCount(p.User.ID)
|
||||
lp, err := app.db.GetUserLastPostTime(p.User.ID)
|
||||
if err != nil {
|
||||
return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get user's last post time: %v", err)}
|
||||
}
|
||||
if lp != nil {
|
||||
p.LastPost = lp.Format("January 2, 2006, 3:04 PM")
|
||||
}
|
||||
|
||||
colls, err := app.db.GetCollections(p.User)
|
||||
if err != nil {
|
||||
return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get user's collections: %v", err)}
|
||||
}
|
||||
for _, c := range *colls {
|
||||
ic := inspectedCollection{
|
||||
CollectionObj: CollectionObj{Collection: c},
|
||||
}
|
||||
|
||||
if app.cfg.App.Federation {
|
||||
folls, err := app.db.GetAPFollowers(&c)
|
||||
if err == nil {
|
||||
// TODO: handle error here (at least log it)
|
||||
ic.Followers = len(*folls)
|
||||
}
|
||||
}
|
||||
|
||||
app.db.GetPostsCount(&ic.CollectionObj, true)
|
||||
|
||||
lp, err := app.db.GetCollectionLastPostTime(c.ID)
|
||||
if err != nil {
|
||||
log.Error("Didn't get last post time for collection %d: %v", c.ID, err)
|
||||
}
|
||||
if lp != nil {
|
||||
ic.LastPost = lp.Format("January 2, 2006, 3:04 PM")
|
||||
}
|
||||
|
||||
p.Colls = append(p.Colls, ic)
|
||||
}
|
||||
|
||||
showUserPage(w, "view-user", p)
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleAdminUpdateSite(app *app, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["page"]
|
||||
|
|
70
database.go
70
database.go
|
@ -112,6 +112,9 @@ type writestore interface {
|
|||
|
||||
GetDynamicContent(id string) (string, *time.Time, error)
|
||||
UpdateDynamicContent(id, content string) error
|
||||
GetAllUsers(page uint) (*[]User, error)
|
||||
GetUserLastPostTime(id int64) (*time.Time, error)
|
||||
GetCollectionLastPostTime(id int64) (*time.Time, error)
|
||||
}
|
||||
|
||||
type datastore struct {
|
||||
|
@ -1740,6 +1743,20 @@ func (db *datastore) GetUserPosts(u *User) (*[]PublicPost, error) {
|
|||
return &posts, nil
|
||||
}
|
||||
|
||||
func (db *datastore) GetUserPostsCount(userID int64) int64 {
|
||||
var count int64
|
||||
err := db.QueryRow("SELECT COUNT(*) FROM posts WHERE owner_id = ?", userID).Scan(&count)
|
||||
switch {
|
||||
case err == sql.ErrNoRows:
|
||||
return 0
|
||||
case err != nil:
|
||||
log.Error("Failed selecting posts count for user %d: %v", userID, err)
|
||||
return 0
|
||||
}
|
||||
|
||||
return count
|
||||
}
|
||||
|
||||
// ChangeSettings takes a User and applies the changes in the given
|
||||
// userSettings, MODIFYING THE USER with successful changes.
|
||||
func (db *datastore) ChangeSettings(app *app, u *User, s *userSettings) error {
|
||||
|
@ -2202,6 +2219,59 @@ func (db *datastore) UpdateDynamicContent(id, content string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
func (db *datastore) GetAllUsers(page uint) (*[]User, error) {
|
||||
const usersPerPage = 30
|
||||
limitStr := fmt.Sprintf("0, %d", usersPerPage)
|
||||
if page > 1 {
|
||||
limitStr = fmt.Sprintf("%d, %d", page*usersPerPage, page*usersPerPage+usersPerPage)
|
||||
}
|
||||
|
||||
rows, err := db.Query("SELECT id, username, created 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."}
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
users := []User{}
|
||||
for rows.Next() {
|
||||
u := User{}
|
||||
err = rows.Scan(&u.ID, &u.Username, &u.Created)
|
||||
if err != nil {
|
||||
log.Error("Failed scanning GetAllUsers() row: %v", err)
|
||||
break
|
||||
}
|
||||
users = append(users, u)
|
||||
}
|
||||
return &users, nil
|
||||
}
|
||||
|
||||
func (db *datastore) GetUserLastPostTime(id int64) (*time.Time, error) {
|
||||
var t time.Time
|
||||
err := db.QueryRow("SELECT created FROM posts WHERE owner_id = ? ORDER BY created DESC LIMIT 1", id).Scan(&t)
|
||||
switch {
|
||||
case err == sql.ErrNoRows:
|
||||
return nil, nil
|
||||
case err != nil:
|
||||
log.Error("Failed selecting last post time from posts: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
return &t, 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)
|
||||
switch {
|
||||
case err == sql.ErrNoRows:
|
||||
return nil, nil
|
||||
case err != nil:
|
||||
log.Error("Failed selecting last post time from posts: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
return &t, nil
|
||||
}
|
||||
|
||||
func stringLogln(log *string, s string, v ...interface{}) {
|
||||
*log += fmt.Sprintf(s+"\n", v...)
|
||||
}
|
||||
|
|
|
@ -2,3 +2,10 @@
|
|||
font-size: 1em;
|
||||
min-height: 12em;
|
||||
}
|
||||
header.admin {
|
||||
margin: 0;
|
||||
|
||||
h1 + a {
|
||||
margin-left: 1em;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -126,6 +126,8 @@ func initRoutes(handler *Handler, r *mux.Router, cfg *config.Config, db *datasto
|
|||
write.HandleFunc("/auth/login", handler.Web(webLogin, UserLevelNoneRequired)).Methods("POST")
|
||||
|
||||
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/update/config", handler.Admin(handleAdminUpdateConfig)).Methods("POST")
|
||||
write.HandleFunc("/admin/update/{page}", handler.Admin(handleAdminUpdateSite)).Methods("POST")
|
||||
|
||||
|
|
|
@ -49,6 +49,7 @@ function savePage(el) {
|
|||
|
||||
<ul class="pagenav">
|
||||
{{if not .SingleUser}}
|
||||
<li><a href="/admin/users">View Users</a></li>
|
||||
<li><a href="#page-about">Edit About page</a></li>
|
||||
<li><a href="#page-privacy">Edit Privacy page</a></li>
|
||||
{{end}}
|
||||
|
|
27
templates/user/admin/users.tmpl
Normal file
27
templates/user/admin/users.tmpl
Normal file
|
@ -0,0 +1,27 @@
|
|||
{{define "users"}}
|
||||
{{template "header" .}}
|
||||
|
||||
<div class="snug content-container">
|
||||
{{template "admin-header" .}}
|
||||
|
||||
<h2 id="posts-header">Users</h2>
|
||||
|
||||
<table class="classy export">
|
||||
<tr>
|
||||
<th>User</th>
|
||||
<th>Joined</th>
|
||||
<th>Type</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>
|
||||
</tr>
|
||||
{{end}}
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
{{template "footer" .}}
|
||||
{{end}}
|
88
templates/user/admin/view-user.tmpl
Normal file
88
templates/user/admin/view-user.tmpl
Normal file
|
@ -0,0 +1,88 @@
|
|||
{{define "view-user"}}
|
||||
{{template "header" .}}
|
||||
<style>
|
||||
table.classy th {
|
||||
text-align: left;
|
||||
}
|
||||
h3 {
|
||||
font-weight: normal;
|
||||
}
|
||||
</style>
|
||||
<div class="snug content-container">
|
||||
{{template "admin-header" .}}
|
||||
<p><a href="/admin/users">View Users</a></p>
|
||||
|
||||
<h2 id="posts-header">{{.User.Username}}</h2>
|
||||
|
||||
<table class="classy export">
|
||||
<tr>
|
||||
<th>No.</th>
|
||||
<td>{{.User.ID}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Type</th>
|
||||
<td>{{if .User.IsAdmin}}Admin{{else}}User{{end}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<td>{{.User.Username}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Joined</th>
|
||||
<td>{{.User.CreatedFriendly}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Total Posts</th>
|
||||
<td>{{.TotalPosts}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Last Post</th>
|
||||
<td>{{if .LastPost}}{{.LastPost}}{{else}}Never{{end}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h2>Blogs</h2>
|
||||
|
||||
{{range .Colls}}
|
||||
<h3><a href="/{{.Alias}}/">{{.Title}}</a></h3>
|
||||
<table class="classy export">
|
||||
<tr>
|
||||
<th>Alias</th>
|
||||
<td>{{.Alias}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<td>{{.Title}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Description</th>
|
||||
<td>{{.Description}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Visibility</th>
|
||||
<td>{{.FriendlyVisibility}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Views</th>
|
||||
<td>{{.Views}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Posts</th>
|
||||
<td>{{.TotalPosts}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Last Post</th>
|
||||
<td>{{if .LastPost}}{{.LastPost}}{{else}}Never{{end}}</td>
|
||||
</tr>
|
||||
{{if $.Config.Federation}}
|
||||
<tr>
|
||||
<th>Fediverse Followers</th>
|
||||
<td>{{.Followers}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</table>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
{{template "footer" .}}
|
||||
{{end}}
|
|
@ -57,3 +57,10 @@
|
|||
</header>
|
||||
<div id="official-writing">
|
||||
{{end}}
|
||||
|
||||
{{define "admin-header"}}
|
||||
<header class="admin">
|
||||
<h1>Admin</h1>
|
||||
<a href="/admin">back to dashboard</a>
|
||||
</header>
|
||||
{{end}}
|
||||
|
|
9
users.go
9
users.go
|
@ -95,6 +95,15 @@ func (u *User) EmailClear(keys *keychain) string {
|
|||
return ""
|
||||
}
|
||||
|
||||
func (u User) CreatedFriendly() string {
|
||||
/*
|
||||
// TODO: accept a locale in this method and use that for the format
|
||||
var loc monday.Locale = monday.LocaleEnUS
|
||||
return monday.Format(u.Created, monday.DateTimeFormatsByLocale[loc], loc)
|
||||
*/
|
||||
return u.Created.Format("January 2, 2006, 3:04 PM")
|
||||
}
|
||||
|
||||
// Cookie strips down an AuthUser to contain only information necessary for
|
||||
// cookies.
|
||||
func (u User) Cookie() *User {
|
||||
|
|
Loading…
Reference in a new issue