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) {
{{if not .SingleUser}}
+ - View Users
- Edit About page
- Edit Privacy page
{{end}}
diff --git a/templates/user/admin/users.tmpl b/templates/user/admin/users.tmpl
new file mode 100644
index 0000000..25a02eb
--- /dev/null
+++ b/templates/user/admin/users.tmpl
@@ -0,0 +1,27 @@
+{{define "users"}}
+{{template "header" .}}
+
+
+ {{template "admin-header" .}}
+
+
+
+
+
+ User |
+ Joined |
+ Type |
+
+ {{range .Users}}
+
+ {{.Username}} |
+ {{.CreatedFriendly}} |
+ {{if .IsAdmin}}Admin{{else}}User{{end}} |
+
+ {{end}}
+
+
+
+
+{{template "footer" .}}
+{{end}}
diff --git a/templates/user/admin/view-user.tmpl b/templates/user/admin/view-user.tmpl
new file mode 100644
index 0000000..5f2a032
--- /dev/null
+++ b/templates/user/admin/view-user.tmpl
@@ -0,0 +1,88 @@
+{{define "view-user"}}
+{{template "header" .}}
+
+
+ {{template "admin-header" .}}
+
View Users
+
+
+
+
+
+ No. |
+ {{.User.ID}} |
+
+
+ Type |
+ {{if .User.IsAdmin}}Admin{{else}}User{{end}} |
+
+
+ Username |
+ {{.User.Username}} |
+
+
+ Joined |
+ {{.User.CreatedFriendly}} |
+
+
+ Total Posts |
+ {{.TotalPosts}} |
+
+
+ Last Post |
+ {{if .LastPost}}{{.LastPost}}{{else}}Never{{end}} |
+
+
+
+
Blogs
+
+ {{range .Colls}}
+
+
+
+ Alias |
+ {{.Alias}} |
+
+
+ Title |
+ {{.Title}} |
+
+
+ Description |
+ {{.Description}} |
+
+
+ Visibility |
+ {{.FriendlyVisibility}} |
+
+
+ Views |
+ {{.Views}} |
+
+
+ Posts |
+ {{.TotalPosts}} |
+
+
+ Last Post |
+ {{if .LastPost}}{{.LastPost}}{{else}}Never{{end}} |
+
+ {{if $.Config.Federation}}
+
+ Fediverse Followers |
+ {{.Followers}} |
+
+ {{end}}
+
+ {{end}}
+
+
+{{template "footer" .}}
+{{end}}
diff --git a/templates/user/include/header.tmpl b/templates/user/include/header.tmpl
index 797b3d7..d28fee4 100644
--- a/templates/user/include/header.tmpl
+++ b/templates/user/include/header.tmpl
@@ -57,3 +57,10 @@
{{end}}
+
+{{define "admin-header"}}
+
+{{end}}
diff --git a/users.go b/users.go
index adc2ea4..69e0efa 100644
--- a/users.go
+++ b/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 {