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
101
admin.go
101
admin.go
|
@ -70,6 +70,12 @@ type systemStatus struct {
|
||||||
NumGC uint32
|
NumGC uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type inspectedCollection struct {
|
||||||
|
CollectionObj
|
||||||
|
Followers int
|
||||||
|
LastPost string
|
||||||
|
}
|
||||||
|
|
||||||
func handleViewAdminDash(app *app, u *User, w http.ResponseWriter, r *http.Request) error {
|
func handleViewAdminDash(app *app, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||||
updateAppStats()
|
updateAppStats()
|
||||||
p := struct {
|
p := struct {
|
||||||
|
@ -104,6 +110,101 @@ func handleViewAdminDash(app *app, u *User, w http.ResponseWriter, r *http.Reque
|
||||||
return nil
|
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 {
|
func handleAdminUpdateSite(app *app, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
id := vars["page"]
|
id := vars["page"]
|
||||||
|
|
70
database.go
70
database.go
|
@ -112,6 +112,9 @@ type writestore interface {
|
||||||
|
|
||||||
GetDynamicContent(id string) (string, *time.Time, error)
|
GetDynamicContent(id string) (string, *time.Time, error)
|
||||||
UpdateDynamicContent(id, content string) 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 {
|
type datastore struct {
|
||||||
|
@ -1740,6 +1743,20 @@ func (db *datastore) GetUserPosts(u *User) (*[]PublicPost, error) {
|
||||||
return &posts, nil
|
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
|
// ChangeSettings takes a User and applies the changes in the given
|
||||||
// userSettings, MODIFYING THE USER with successful changes.
|
// userSettings, MODIFYING THE USER with successful changes.
|
||||||
func (db *datastore) ChangeSettings(app *app, u *User, s *userSettings) error {
|
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
|
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{}) {
|
func stringLogln(log *string, s string, v ...interface{}) {
|
||||||
*log += fmt.Sprintf(s+"\n", v...)
|
*log += fmt.Sprintf(s+"\n", v...)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,3 +2,10 @@
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
min-height: 12em;
|
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("/auth/login", handler.Web(webLogin, UserLevelNoneRequired)).Methods("POST")
|
||||||
|
|
||||||
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/user/{username}", handler.Admin(handleViewAdminUser)).Methods("GET")
|
||||||
write.HandleFunc("/admin/update/config", handler.Admin(handleAdminUpdateConfig)).Methods("POST")
|
write.HandleFunc("/admin/update/config", handler.Admin(handleAdminUpdateConfig)).Methods("POST")
|
||||||
write.HandleFunc("/admin/update/{page}", handler.Admin(handleAdminUpdateSite)).Methods("POST")
|
write.HandleFunc("/admin/update/{page}", handler.Admin(handleAdminUpdateSite)).Methods("POST")
|
||||||
|
|
||||||
|
|
|
@ -49,6 +49,7 @@ function savePage(el) {
|
||||||
|
|
||||||
<ul class="pagenav">
|
<ul class="pagenav">
|
||||||
{{if not .SingleUser}}
|
{{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-about">Edit About page</a></li>
|
||||||
<li><a href="#page-privacy">Edit Privacy page</a></li>
|
<li><a href="#page-privacy">Edit Privacy page</a></li>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
@ -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}}
|
|
@ -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>
|
</header>
|
||||||
<div id="official-writing">
|
<div id="official-writing">
|
||||||
{{end}}
|
{{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 ""
|
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
|
// Cookie strips down an AuthUser to contain only information necessary for
|
||||||
// cookies.
|
// cookies.
|
||||||
func (u User) Cookie() *User {
|
func (u User) Cookie() *User {
|
||||||
|
|
Loading…
Reference in New Issue