From 0e722de82c849ed8227767a65647c962ae700f7f Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Fri, 4 Jan 2019 22:28:29 -0500 Subject: [PATCH] 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 --- admin.go | 101 ++++++++++++++++++++++++++++ database.go | 70 +++++++++++++++++++ less/admin.less | 7 ++ routes.go | 2 + templates/user/admin.tmpl | 1 + templates/user/admin/users.tmpl | 27 ++++++++ templates/user/admin/view-user.tmpl | 88 ++++++++++++++++++++++++ templates/user/include/header.tmpl | 7 ++ users.go | 9 +++ 9 files changed, 312 insertions(+) create mode 100644 templates/user/admin/users.tmpl create mode 100644 templates/user/admin/view-user.tmpl diff --git a/admin.go b/admin.go index 3a8665b..79dc4ae 100644 --- a/admin.go +++ b/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"] diff --git a/database.go b/database.go index 250b6e0..0824978 100644 --- a/database.go +++ b/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...) } diff --git a/less/admin.less b/less/admin.less index 4f6e8ac..d4c8f6e 100644 --- a/less/admin.less +++ b/less/admin.less @@ -2,3 +2,10 @@ font-size: 1em; min-height: 12em; } +header.admin { + margin: 0; + + h1 + a { + margin-left: 1em; + } +} diff --git a/routes.go b/routes.go index e11d51f..988bd43 100644 --- a/routes.go +++ b/routes.go @@ -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") diff --git a/templates/user/admin.tmpl b/templates/user/admin.tmpl index 9c4c169..223b3c1 100644 --- a/templates/user/admin.tmpl +++ b/templates/user/admin.tmpl @@ -49,6 +49,7 @@ function savePage(el) {