diff --git a/pkg/controller/account/deactivate.go b/pkg/controller/account/deactivate.go new file mode 100644 index 0000000..cfefa8c --- /dev/null +++ b/pkg/controller/account/deactivate.go @@ -0,0 +1,82 @@ +package account + +import ( + "net/http" + "strings" + + "code.nonshy.com/nonshy/website/pkg/models" + "code.nonshy.com/nonshy/website/pkg/session" + "code.nonshy.com/nonshy/website/pkg/templates" +) + +// Deactivate account page (self service). +func Deactivate() http.HandlerFunc { + tmpl := templates.Must("account/deactivate.html") + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + currentUser, err := session.CurrentUser(r) + if err != nil { + session.FlashError(w, r, "Couldn't get your current user: %s", err) + templates.Redirect(w, "/") + return + } + + // Confirm deletion. + if r.Method == http.MethodPost { + var password = strings.TrimSpace(r.PostFormValue("password")) + if err := currentUser.CheckPassword(password); err != nil { + session.FlashError(w, r, "You must enter your correct account password to delete your account.") + templates.Redirect(w, r.URL.Path) + return + } + + // Deactivate their account! + currentUser.Status = models.UserStatusDisabled + if err := currentUser.Save(); err != nil { + session.FlashError(w, r, "Error while deactivating your account: %s", err) + templates.Redirect(w, r.URL.Path) + return + } + + // Sign them out. + session.LogoutUser(w, r) + session.Flash(w, r, "Your account has been deactivated and you are now logged out. If you wish to re-activate your account, sign in again with your username and password.") + templates.Redirect(w, "/") + return + } + + var vars = map[string]interface{}{} + if err := tmpl.Execute(w, r, vars); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }) +} + +// Reactivate account page +func Reactivate() http.HandlerFunc { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + currentUser, err := session.CurrentUser(r) + if err != nil { + session.FlashError(w, r, "Couldn't get your current user: %s", err) + templates.Redirect(w, "/") + return + } + + if currentUser.Status != models.UserStatusDisabled { + session.FlashError(w, r, "Your account was not disabled in the first place!") + templates.Redirect(w, "/") + return + } + + // Reactivate them. + currentUser.Status = models.UserStatusActive + if err := currentUser.Save(); err != nil { + session.FlashError(w, r, "Error while reactivating your account: %s", err) + templates.Redirect(w, "/") + return + } + + session.Flash(w, r, "Welcome back! Your account has been reactivated.") + templates.Redirect(w, "/") + }) +} diff --git a/pkg/controller/account/friends.go b/pkg/controller/account/friends.go new file mode 100644 index 0000000..0922a86 --- /dev/null +++ b/pkg/controller/account/friends.go @@ -0,0 +1,88 @@ +package account + +import ( + "net/http" + "regexp" + + "code.nonshy.com/nonshy/website/pkg/config" + "code.nonshy.com/nonshy/website/pkg/models" + "code.nonshy.com/nonshy/website/pkg/session" + "code.nonshy.com/nonshy/website/pkg/templates" +) + +var UserFriendsRegexp = regexp.MustCompile(`^/friends/u/([^@]+?)$`) + +// User friends page (/friends/u/username) +func UserFriends() http.HandlerFunc { + tmpl := templates.Must("account/friends.html") + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Parse the username out of the URL parameters. + var username string + m := UserFriendsRegexp.FindStringSubmatch(r.URL.Path) + if m != nil { + username = m[1] + } + + // Find this user. + user, err := models.FindUser(username) + if err != nil { + templates.NotFoundPage(w, r) + return + } + + // Get the viewer. + currentUser, err := session.CurrentUser(r) + if err != nil { + session.FlashError(w, r, "Unexpected error: could not get currentUser.") + templates.Redirect(w, "/") + return + } + + var isSelf = currentUser.ID == user.ID + + // Banned or disabled? Only admin can view then. + if user.Status != models.UserStatusActive && !currentUser.IsAdmin { + templates.NotFoundPage(w, r) + return + } + + // Is either one blocking? + if models.IsBlocking(currentUser.ID, user.ID) && !currentUser.IsAdmin { + templates.NotFoundPage(w, r) + return + } + + // Get their friends. + pager := &models.Pagination{ + PerPage: config.PageSizeFriends, + Sort: "updated_at desc", + } + pager.ParsePage(r) + friends, err := models.PaginateOtherUserFriends(currentUser, user, pager) + if err != nil { + session.FlashError(w, r, "Couldn't paginate friends: %s", err) + templates.Redirect(w, "/") + return + } + + // Inject relationship booleans. + models.SetUserRelationships(currentUser, friends) + + var vars = map[string]interface{}{ + "User": user, + "IsSelf": isSelf, + "PhotoCount": models.CountPhotosICanSee(user, currentUser), + "NoteCount": models.CountNotesAboutUser(currentUser, user), + "FriendCount": models.CountFriends(user.ID), + "Friends": friends, + "Pager": pager, + + // Map our friendships to these users. + "FriendMap": models.MapFriends(currentUser, friends), + } + if err := tmpl.Execute(w, r, vars); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }) +} diff --git a/pkg/controller/account/login.go b/pkg/controller/account/login.go index 401c96f..8a020c3 100644 --- a/pkg/controller/account/login.go +++ b/pkg/controller/account/login.go @@ -64,8 +64,8 @@ func Login() http.HandlerFunc { return } - // Is their account banned or disabled? - if user.Status != models.UserStatusActive { + // Is their account banned? + if user.Status == models.UserStatusBanned { session.FlashError(w, r, "Your account has been %s. If you believe this was done in error, please contact support.", user.Status) templates.Redirect(w, r.URL.Path) return diff --git a/pkg/controller/account/profile.go b/pkg/controller/account/profile.go index 8cc22d6..2707412 100644 --- a/pkg/controller/account/profile.go +++ b/pkg/controller/account/profile.go @@ -114,13 +114,14 @@ func Profile() http.HandlerFunc { } vars := map[string]interface{}{ - "User": user, - "LikeMap": likeMap, - "IsFriend": isFriend, - "IsPrivate": isPrivate, - "PhotoCount": models.CountPhotosICanSee(user, currentUser), - "NoteCount": models.CountNotesAboutUser(currentUser, user), - "OnChat": worker.GetChatStatistics().IsOnline(user.Username), + "User": user, + "LikeMap": likeMap, + "IsFriend": isFriend, + "IsPrivate": isPrivate, + "PhotoCount": models.CountPhotosICanSee(user, currentUser), + "NoteCount": models.CountNotesAboutUser(currentUser, user), + "FriendCount": models.CountFriends(user.ID), + "OnChat": worker.GetChatStatistics().IsOnline(user.Username), // Details on who likes the photo. "LikeExample": likeExample, diff --git a/pkg/controller/account/user_note.go b/pkg/controller/account/user_note.go index f2c3e5f..abd9aa3 100644 --- a/pkg/controller/account/user_note.go +++ b/pkg/controller/account/user_note.go @@ -139,10 +139,11 @@ func UserNotes() http.HandlerFunc { } vars := map[string]interface{}{ - "User": user, - "PhotoCount": models.CountPhotosICanSee(user, currentUser), - "NoteCount": models.CountNotesAboutUser(currentUser, user), - "MyNote": myNote, + "User": user, + "PhotoCount": models.CountPhotosICanSee(user, currentUser), + "NoteCount": models.CountNotesAboutUser(currentUser, user), + "FriendCount": models.CountFriends(user.ID), + "MyNote": myNote, // Admin concerns. "Feedback": feedback, diff --git a/pkg/controller/chat/chat.go b/pkg/controller/chat/chat.go index 6866d9e..58bfd0c 100644 --- a/pkg/controller/chat/chat.go +++ b/pkg/controller/chat/chat.go @@ -191,7 +191,7 @@ func FilteredChatStatistics(currentUser *models.User) worker.ChatStatistics { // Who are we blocking? var blockedUsernames = map[string]interface{}{} - for _, username := range models.BlockedUsernames(currentUser.ID) { + for _, username := range models.BlockedUsernames(currentUser) { blockedUsernames[username] = nil } @@ -211,7 +211,7 @@ func FilteredChatStatistics(currentUser *models.User) worker.ChatStatistics { // SendBlocklist syncs the user blocklist to the chat server prior to sending them over. func SendBlocklist(user *models.User) error { // Get the user's blocklist. - blockedUsernames := models.BlockedUsernames(user.ID) + blockedUsernames := models.BlockedUsernames(user) log.Info("SendBlocklist(%s) to BareRTC: %d blocked usernames", user.Username, len(blockedUsernames)) // API request struct for BareRTC /api/blocklist endpoint. diff --git a/pkg/controller/inbox/inbox.go b/pkg/controller/inbox/inbox.go index bb55186..75a895a 100644 --- a/pkg/controller/inbox/inbox.go +++ b/pkg/controller/inbox/inbox.go @@ -95,13 +95,13 @@ func Inbox() http.HandlerFunc { // On the main inbox view, ?page= params page thru the message list, not a thread. pager.ParsePage(r) } - messages, err := models.GetMessages(currentUser.ID, showSent, pager) + messages, err := models.GetMessages(currentUser, showSent, pager) if err != nil { session.FlashError(w, r, "Couldn't get your messages from DB: %s", err) } // How many unreads? - unread, err := models.CountUnreadMessages(currentUser.ID) + unread, err := models.CountUnreadMessages(currentUser) if err != nil { session.FlashError(w, r, "Couldn't get your unread message count from DB: %s", err) } diff --git a/pkg/controller/photo/user_gallery.go b/pkg/controller/photo/user_gallery.go index 0821b7a..386609d 100644 --- a/pkg/controller/photo/user_gallery.go +++ b/pkg/controller/photo/user_gallery.go @@ -149,6 +149,7 @@ func UserPhotos() http.HandlerFunc { "Photos": photos, "PhotoCount": models.CountPhotosICanSee(user, currentUser), "NoteCount": models.CountNotesAboutUser(currentUser, user), + "FriendCount": models.CountFriends(user.ID), "PublicPhotoCount": models.CountPublicPhotos(user.ID), "InnerCircleMinimumPublicPhotos": config.InnerCircleMinimumPublicPhotos, "Pager": pager, diff --git a/pkg/middleware/authentication.go b/pkg/middleware/authentication.go index 5e68444..f02d96b 100644 --- a/pkg/middleware/authentication.go +++ b/pkg/middleware/authentication.go @@ -27,19 +27,19 @@ func LoginRequired(handler http.Handler) http.Handler { return } - // Are they banned or disabled? - if user.Status == models.UserStatusDisabled { - session.LogoutUser(w, r) - session.FlashError(w, r, "Your account has been disabled and you are now logged out.") - templates.Redirect(w, "/") - return - } else if user.Status == models.UserStatusBanned { + // Are they banned? + if user.Status == models.UserStatusBanned { session.LogoutUser(w, r) session.FlashError(w, r, "Your account has been banned and you are now logged out.") templates.Redirect(w, "/") return } + // Is their account disabled? + if DisabledAccount(user, w, r) { + return + } + // Is the site under a Maintenance Mode restriction? if MaintenanceMode(user, w, r) { return @@ -115,19 +115,19 @@ func CertRequired(handler http.Handler) http.Handler { return } - // Are they banned or disabled? - if currentUser.Status == models.UserStatusDisabled { - session.LogoutUser(w, r) - session.FlashError(w, r, "Your account has been disabled and you are now logged out.") - templates.Redirect(w, "/") - return - } else if currentUser.Status == models.UserStatusBanned { + // Are they banned? + if currentUser.Status == models.UserStatusBanned { session.LogoutUser(w, r) session.FlashError(w, r, "Your account has been banned and you are now logged out.") templates.Redirect(w, "/") return } + // Is their account disabled? + if DisabledAccount(currentUser, w, r) { + return + } + // Is the site under a Maintenance Mode restriction? if MaintenanceMode(currentUser, w, r) { return diff --git a/pkg/middleware/disabled_account.go b/pkg/middleware/disabled_account.go new file mode 100644 index 0000000..5fd5d84 --- /dev/null +++ b/pkg/middleware/disabled_account.go @@ -0,0 +1,39 @@ +package middleware + +import ( + "net/http" + "strings" + + "code.nonshy.com/nonshy/website/pkg/models" + "code.nonshy.com/nonshy/website/pkg/templates" +) + +var tmplDisabledAccount = templates.Must("errors/disabled_account.html") + +// Whitelist of paths to allow disabled accounts to access. +var disabledAccountPathWhitelist = []string{ + "/account/delete", + "/account/reactivate", +} + +// DisabledAccount check that limits a logged-in user's options to either reactivate their account, +// delete it, or log back out. +func DisabledAccount(currentUser *models.User, w http.ResponseWriter, r *http.Request) bool { + // Is their account disabled? + if currentUser.Status == models.UserStatusDisabled { + // Whitelisted paths? + for _, path := range disabledAccountPathWhitelist { + if strings.HasPrefix(r.URL.Path, path) { + return false + } + } + + // Show the disabled account page to all other requests. + if err := tmplDisabledAccount.Execute(w, r, nil); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + return true + } + + return false +} diff --git a/pkg/models/blocklist.go b/pkg/models/blocklist.go index 860b740..f3e169b 100644 --- a/pkg/models/blocklist.go +++ b/pkg/models/blocklist.go @@ -93,27 +93,36 @@ func PaginateBlockList(user *User, pager *Pagination) ([]*User, error) { } // BlockedUserIDs returns all user IDs blocked by the user (bidirectional, source or target user). -func BlockedUserIDs(userId uint64) []uint64 { +func BlockedUserIDs(user *User) []uint64 { + // Have we looked this up already on this request? + if user.cacheBlockedUserIDs != nil { + return user.cacheBlockedUserIDs + } + var ( bs = []*Block{} userIDs = []uint64{} ) - DB.Where("source_user_id = ? OR target_user_id = ?", userId, userId).Find(&bs) + DB.Where("source_user_id = ? OR target_user_id = ?", user.ID, user.ID).Find(&bs) for _, row := range bs { for _, uid := range []uint64{row.TargetUserID, row.SourceUserID} { - if uid != userId { + if uid != user.ID { userIDs = append(userIDs, uid) } } } + + // Cache the result in the User so we don't query it again. + user.cacheBlockedUserIDs = userIDs + return userIDs } // MapBlockedUserIDs returns BlockedUserIDs as a lookup hashmap (not for front-end templates currently). -func MapBlockedUserIDs(userId uint64) map[uint64]interface{} { +func MapBlockedUserIDs(user *User) map[uint64]interface{} { var ( result = map[uint64]interface{}{} - blockedIDs = BlockedUserIDs(userId) + blockedIDs = BlockedUserIDs(user) ) for _, uid := range blockedIDs { result[uid] = nil @@ -125,7 +134,7 @@ func MapBlockedUserIDs(userId uint64) map[uint64]interface{} { func FilterBlockingUserIDs(currentUser *User, userIDs []uint64) []uint64 { var ( // Get the IDs to exclude. - blockedIDs = MapBlockedUserIDs(currentUser.ID) + blockedIDs = MapBlockedUserIDs(currentUser) // Filter the result. result = []uint64{} @@ -157,9 +166,9 @@ func BlockedUserIDsByUser(userId uint64) []uint64 { } // BlockedUsernames returns all usernames blocked by (or blocking) the user. -func BlockedUsernames(userId uint64) []string { +func BlockedUsernames(user *User) []string { var ( - userIDs = BlockedUserIDs(userId) + userIDs = BlockedUserIDs(user) usernames = []string{} ) @@ -174,7 +183,7 @@ func BlockedUsernames(userId uint64) []string { ).Where( "id IN ?", userIDs, ).Scan(&usernames); res.Error != nil { - log.Error("BlockedUsernames(%d): %s", userId, res.Error) + log.Error("BlockedUsernames(%d): %s", user.Username, res.Error) } return usernames diff --git a/pkg/models/comment.go b/pkg/models/comment.go index 81dc339..982a9c7 100644 --- a/pkg/models/comment.go +++ b/pkg/models/comment.go @@ -71,7 +71,7 @@ func PaginateComments(user *User, tableName string, tableID uint64, pager *Pagin var ( cs = []*Comment{} query = (&Comment{}).Preload() - blockedUserIDs = BlockedUserIDs(user.ID) + blockedUserIDs = BlockedUserIDs(user) wheres = []string{} placeholders = []interface{}{} ) @@ -84,6 +84,16 @@ func PaginateComments(user *User, tableName string, tableID uint64, pager *Pagin placeholders = append(placeholders, blockedUserIDs) } + // Don't show comments from banned or disabled accounts. + wheres = append(wheres, ` + EXISTS ( + SELECT 1 + FROM users + WHERE users.id = comments.user_id + AND users.status = 'active' + ) + `) + query = query.Where( strings.Join(wheres, " AND "), placeholders..., @@ -102,7 +112,7 @@ func PaginateComments(user *User, tableName string, tableID uint64, pager *Pagin func ListComments(user *User, tableName string, tableID uint64) ([]*Comment, error) { var ( cs []*Comment - blockedUserIDs = BlockedUserIDs(user.ID) + blockedUserIDs = BlockedUserIDs(user) wheres = []string{} placeholders = []interface{}{} ) @@ -115,6 +125,16 @@ func ListComments(user *User, tableName string, tableID uint64) ([]*Comment, err placeholders = append(placeholders, blockedUserIDs) } + // Don't show comments from banned or disabled accounts. + wheres = append(wheres, ` + EXISTS ( + SELECT 1 + FROM users + WHERE users.id = comments.user_id + AND users.status = 'active' + ) + `) + result := (&Comment{}).Preload().Where( strings.Join(wheres, " AND "), placeholders..., diff --git a/pkg/models/forum_recent.go b/pkg/models/forum_recent.go index 6ba5cfd..cfa4928 100644 --- a/pkg/models/forum_recent.go +++ b/pkg/models/forum_recent.go @@ -22,10 +22,11 @@ type RecentPost struct { // PaginateRecentPosts returns all of the comments on a forum paginated. func PaginateRecentPosts(user *User, categories []string, pager *Pagination) ([]*RecentPost, error) { var ( - result = []*RecentPost{} - query = (&Comment{}).Preload() - wheres = []string{"table_name = 'threads'"} - placeholders = []interface{}{} + result = []*RecentPost{} + query = (&Comment{}).Preload() + blockedUserIDs = BlockedUserIDs(user) + wheres = []string{"table_name = 'threads'"} + placeholders = []interface{}{} ) if len(categories) > 0 { @@ -43,6 +44,22 @@ func PaginateRecentPosts(user *User, categories []string, pager *Pagination) ([] wheres = append(wheres, "forums.inner_circle is not true") } + // Blocked users? + if len(blockedUserIDs) > 0 { + wheres = append(wheres, "comments.user_id NOT IN ?") + placeholders = append(placeholders, blockedUserIDs) + } + + // Don't show comments from banned or disabled accounts. + wheres = append(wheres, ` + EXISTS ( + SELECT 1 + FROM users + WHERE users.id = comments.user_id + AND users.status = 'active' + ) + `) + // Get the page of recent forum comment IDs of all time. type scanner struct { CommentID uint64 @@ -130,7 +147,7 @@ func PaginateRecentPosts(user *User, categories []string, pager *Pagination) ([] comments, _ = GetComments(commentIDs) } if len(threadIDs) > 0 { - threads, _ = GetThreads(threadIDs) + threads, _ = GetThreadsAsUser(user, threadIDs) } if len(forumIDs) > 0 { forums, _ = GetForums(forumIDs) @@ -154,6 +171,14 @@ func PaginateRecentPosts(user *User, categories []string, pager *Pagination) ([] thrs = append(thrs, thr) } else { log.Error("RecentPosts: didn't find thread ID %d in map!", rc.ThreadID) + + // Create a dummy placeholder Thread (e.g.: the thread originator has been + // banned or disabled, but the thread summary is shown on the new comment view) + rc.Thread = &Thread{ + Comment: Comment{ + Message: "[unavailable]", + }, + } } if f, ok := forums[rc.ForumID]; ok { diff --git a/pkg/models/friend.go b/pkg/models/friend.go index d0ae1fa..b97b190 100644 --- a/pkg/models/friend.go +++ b/pkg/models/friend.go @@ -2,6 +2,7 @@ package models import ( "errors" + "strings" "time" "code.nonshy.com/nonshy/website/pkg/log" @@ -246,6 +247,20 @@ func CountFriendRequests(userID uint64) (int64, error) { return count, result.Error } +// CountFriends gets a count of friends for the user. +func CountFriends(userID uint64) int64 { + var count int64 + result := DB.Where( + "target_user_id = ? AND approved = ? AND EXISTS (SELECT 1 FROM users WHERE users.id = friends.source_user_id AND users.status = 'active')", + userID, + true, + ).Model(&Friend{}).Count(&count) + if result.Error != nil { + log.Error("CountFriends(%d): %s", userID, result.Error) + } + return count +} + /* PaginateFriends gets a page of friends (or pending friend requests) as User objects ordered by friendship date. @@ -302,6 +317,62 @@ func PaginateFriends(user *User, requests bool, sent bool, pager *Pagination) ([ return GetUsers(user, userIDs) } +/* +PaginateOtherUserFriends gets a page of friends from another user, for their profile page. +*/ +func PaginateOtherUserFriends(currentUser *User, user *User, pager *Pagination) ([]*User, error) { + // We paginate over the Friend table. + var ( + fs = []*Friend{} + blockedUserIDs = BlockedUserIDs(currentUser) + userIDs = []uint64{} + wheres = []string{} + placeholders = []interface{}{} + query = DB.Model(&Friend{}) + ) + + // Get friends of the target user. + wheres = append(wheres, "source_user_id = ? AND approved = ?") + placeholders = append(placeholders, user.ID, true) + + // Don't show our blocked users in the result. + if len(blockedUserIDs) > 0 { + wheres = append(wheres, "target_user_id NOT IN ?") + placeholders = append(placeholders, blockedUserIDs) + } + + // Don't show disabled or banned users. + wheres = append(wheres, ` + EXISTS ( + SELECT 1 + FROM users + WHERE users.id = friends.target_user_id + AND users.status = 'active' + ) + `) + + query = query.Where( + strings.Join(wheres, " AND "), + placeholders..., + ).Order(pager.Sort) + + // Get the total count. + query.Count(&pager.Total) + + // Get the page. + result := query.Offset(pager.GetOffset()).Limit(pager.PerPage).Find(&fs) + if result.Error != nil { + return nil, result.Error + } + + // Now of these friends get their User objects. + for _, friend := range fs { + userIDs = append(userIDs, friend.TargetUserID) + } + + return GetUsers(user, userIDs) +} + // GetFriendRequests returns all pending friend requests for a user. func GetFriendRequests(userID uint64) ([]*Friend, error) { var fs = []*Friend{} diff --git a/pkg/models/like.go b/pkg/models/like.go index 418fd54..19707e2 100644 --- a/pkg/models/like.go +++ b/pkg/models/like.go @@ -73,7 +73,7 @@ func WhoLikes(currentUser *User, tableName string, tableID uint64) ([]*User, int remainder = total wheres = []string{} placeholders = []interface{}{} - blockedUserIDs = BlockedUserIDs(currentUser.ID) + blockedUserIDs = BlockedUserIDs(currentUser) ) wheres = append(wheres, "table_name = ? AND table_id = ?") @@ -117,7 +117,7 @@ func PaginateLikes(currentUser *User, tableName string, tableID uint64, pager *P userIDs = []uint64{} wheres = []string{} placeholders = []interface{}{} - blockedUserIDs = BlockedUserIDs(currentUser.ID) + blockedUserIDs = BlockedUserIDs(currentUser) ) wheres = append(wheres, "table_name = ? AND table_id = ?") diff --git a/pkg/models/message.go b/pkg/models/message.go index fd0f884..13d6372 100644 --- a/pkg/models/message.go +++ b/pkg/models/message.go @@ -24,17 +24,17 @@ func GetMessage(id uint64) (*Message, error) { } // GetMessages for a user. -func GetMessages(userID uint64, sent bool, pager *Pagination) ([]*Message, error) { +func GetMessages(user *User, sent bool, pager *Pagination) ([]*Message, error) { var ( m = []*Message{} - blockedUserIDs = BlockedUserIDs(userID) + blockedUserIDs = BlockedUserIDs(user) where = []string{} placeholders = []interface{}{} ) if sent { where = append(where, "source_user_id = ?") - placeholders = append(placeholders, userID) + placeholders = append(placeholders, user.ID) if len(blockedUserIDs) > 0 { where = append(where, "target_user_id NOT IN ?") @@ -42,7 +42,7 @@ func GetMessages(userID uint64, sent bool, pager *Pagination) ([]*Message, error } } else { where = append(where, "target_user_id = ?") - placeholders = append(placeholders, userID) + placeholders = append(placeholders, user.ID) if len(blockedUserIDs) > 0 { where = append(where, "source_user_id NOT IN ?") @@ -50,6 +50,16 @@ func GetMessages(userID uint64, sent bool, pager *Pagination) ([]*Message, error } } + // Don't show messages from banned or disabled accounts. + where = append(where, ` + NOT EXISTS ( + SELECT 1 + FROM users + WHERE users.id IN (messages.target_user_id, messages.source_user_id) + AND users.status <> 'active' + ) + `) + query := DB.Where( strings.Join(where, " AND "), placeholders..., @@ -100,11 +110,36 @@ func DeleteMessageThread(message *Message) error { } // CountUnreadMessages gets the count of unread messages for a user. -func CountUnreadMessages(userID uint64) (int64, error) { +func CountUnreadMessages(user *User) (int64, error) { + var ( + blockedUserIDs = BlockedUserIDs(user) + where = []string{ + "target_user_id = ? AND read = ?", + } + placeholders = []interface{}{ + user.ID, false, + } + ) + + // Blocking user IDs? + if len(blockedUserIDs) > 0 { + where = append(where, "source_user_id NOT IN ?") + placeholders = append(placeholders, blockedUserIDs) + } + + // Don't show messages from banned or disabled accounts. + where = append(where, ` + NOT EXISTS ( + SELECT 1 + FROM users + WHERE users.id IN (messages.target_user_id, messages.source_user_id) + AND users.status <> 'active' + ) + `) + query := DB.Where( - "target_user_id = ? AND read = ?", - userID, - false, + strings.Join(where, " AND "), + placeholders..., ) var count int64 diff --git a/pkg/models/notification.go b/pkg/models/notification.go index 127952b..73f2dcd 100644 --- a/pkg/models/notification.go +++ b/pkg/models/notification.go @@ -1,6 +1,7 @@ package models import ( + "strings" "time" "code.nonshy.com/nonshy/website/pkg/log" @@ -139,11 +140,36 @@ func ClearAllNotifications(user *User) error { } // CountUnreadNotifications gets the count of unread Notifications for a user. -func CountUnreadNotifications(userID uint64) (int64, error) { +func CountUnreadNotifications(user *User) (int64, error) { + var ( + blockedUserIDs = BlockedUserIDs(user) + where = []string{ + "user_id = ? AND read = ?", + } + placeholders = []interface{}{ + user.ID, false, + } + ) + + // Blocking user IDs? + if len(blockedUserIDs) > 0 { + where = append(where, "about_user_id NOT IN ?") + placeholders = append(placeholders, blockedUserIDs) + } + + // Don't show messages from banned or disabled accounts. + where = append(where, ` + EXISTS ( + SELECT 1 + FROM users + WHERE users.id = notifications.about_user_id + AND users.status = 'active' + ) + `) + query := DB.Where( - "user_id = ? AND read = ?", - userID, - false, + strings.Join(where, " AND "), + placeholders..., ) var count int64 @@ -153,11 +179,36 @@ func CountUnreadNotifications(userID uint64) (int64, error) { // PaginateNotifications returns the user's notifications. func PaginateNotifications(user *User, pager *Pagination) ([]*Notification, error) { - var ns = []*Notification{} + var ( + ns = []*Notification{} + blockedUserIDs = BlockedUserIDs(user) + where = []string{ + "user_id = ?", + } + placeholders = []interface{}{ + user.ID, + } + ) + + // Suppress historic notifications about blocked users. + if len(blockedUserIDs) > 0 { + where = append(where, "about_user_id NOT IN ?") + placeholders = append(placeholders, blockedUserIDs) + } + + // Don't show notifications from banned or disabled accounts. + where = append(where, ` + EXISTS ( + SELECT 1 + FROM users + WHERE users.id = notifications.about_user_id + AND users.status = 'active' + ) + `) query := (&Notification{}).Preload().Where( - "user_id = ?", - user.ID, + strings.Join(where, " AND "), + placeholders..., ).Order( pager.Sort, ) @@ -348,8 +399,6 @@ func (nm NotificationMap) mapNotificationComments(IDs []uint64) { commentIDs = append(commentIDs, row.CommentID) } - log.Error("MAP COMMENT IDS GOT: %+v", commentIDs) - // Load the comments. if len(commentIDs) > 0 { if comments, err := GetComments(commentIDs); err != nil { diff --git a/pkg/models/photo.go b/pkg/models/photo.go index 6c67162..d86eb6e 100644 --- a/pkg/models/photo.go +++ b/pkg/models/photo.go @@ -397,7 +397,7 @@ func PaginateGalleryPhotos(user *User, conf Gallery, pager *Pagination) ([]*Phot userID = user.ID explicitOK = user.Explicit // User opted-in for explicit content - blocklist = BlockedUserIDs(userID) + blocklist = BlockedUserIDs(user) friendIDs = FriendIDs(userID) privateUserIDs = PrivateGrantedUserIDs(userID) wheres = []string{} diff --git a/pkg/models/thread.go b/pkg/models/thread.go index d31768f..4a16aca 100644 --- a/pkg/models/thread.go +++ b/pkg/models/thread.go @@ -45,11 +45,67 @@ func GetThread(id uint64) (*Thread, error) { // GetThreads queries a set of thread IDs and returns them mapped. func GetThreads(IDs []uint64) (map[uint64]*Thread, error) { var ( - mt = map[uint64]*Thread{} - ts = []*Thread{} + mt = map[uint64]*Thread{} + ts = []*Thread{} + wheres = []string{"threads.id IN ?"} + placeholders = []interface{}{IDs} ) - result := (&Thread{}).Preload().Where("id IN ?", IDs).Find(&ts) + // Don't show threads from banned or disabled accounts. + wheres = append(wheres, ` + EXISTS ( + SELECT 1 + FROM users + WHERE users.id = comments.user_id + AND users.status = 'active' + ) + `) + + result := (&Thread{}).Preload().Joins( + "LEFT OUTER JOIN comments ON (comments.id = threads.comment_id)", + ).Where( + strings.Join(wheres, " AND "), + placeholders..., + ).Find(&ts) + for _, row := range ts { + mt[row.ID] = row + } + + return mt, result.Error +} + +// GetThreadsAsUser queries a set of thread IDs and returns them mapped, taking blocklists into consideration. +func GetThreadsAsUser(currentUser *User, IDs []uint64) (map[uint64]*Thread, error) { + var ( + mt = map[uint64]*Thread{} + ts = []*Thread{} + blockedUserIDs = BlockedUserIDs(currentUser) + wheres = []string{"threads.id IN ?"} + placeholders = []interface{}{IDs} + ) + + // Blocked users? + if len(blockedUserIDs) > 0 { + wheres = append(wheres, "comments.user_id NOT IN ?") + placeholders = append(placeholders, blockedUserIDs) + } + + // Don't show threads from banned or disabled accounts. + wheres = append(wheres, ` + EXISTS ( + SELECT 1 + FROM users + WHERE users.id = comments.user_id + AND users.status = 'active' + ) + `) + + result := (&Thread{}).Preload().Joins( + "LEFT OUTER JOIN comments ON (comments.id = threads.comment_id)", + ).Where( + strings.Join(wheres, " AND "), + placeholders..., + ).Find(&ts) for _, row := range ts { mt[row.ID] = row } @@ -170,7 +226,19 @@ func PaginateThreads(user *User, forum *Forum, pager *Pagination) ([]*Thread, er wheres = append(wheres, "explicit IS NOT TRUE") } - query = query.Where( + // Don't show threads from banned or disabled accounts. + wheres = append(wheres, ` + EXISTS ( + SELECT 1 + FROM users + WHERE users.id = comments.user_id + AND users.status = 'active' + ) + `) + + query = query.Joins( + "LEFT OUTER JOIN comments ON (comments.id = threads.comment_id)", + ).Where( strings.Join(wheres, " AND "), placeholders..., ).Order(pager.Sort) diff --git a/pkg/models/user.go b/pkg/models/user.go index a708013..2a596af 100644 --- a/pkg/models/user.go +++ b/pkg/models/user.go @@ -43,7 +43,8 @@ type User struct { UserRelationship UserRelationship `gorm:"-"` // Caches - cachePhotoTypes map[PhotoVisibility]struct{} + cachePhotoTypes map[PhotoVisibility]struct{} + cacheBlockedUserIDs []uint64 } type UserVisibility string @@ -231,7 +232,7 @@ func SearchUsers(user *User, search *UserSearch, pager *Pagination) ([]*User, er joins string // GPS location join. wheres = []string{} placeholders = []interface{}{} - blockedUserIDs = BlockedUserIDs(user.ID) + blockedUserIDs = BlockedUserIDs(user) myLocation = GetUserLocation(user.ID) ) diff --git a/pkg/router/router.go b/pkg/router/router.go index d77277f..95ec68c 100644 --- a/pkg/router/router.go +++ b/pkg/router/router.go @@ -47,6 +47,8 @@ func New() http.Handler { mux.Handle("/settings/age-gate", middleware.LoginRequired(account.AgeGate())) mux.Handle("/account/two-factor/setup", middleware.LoginRequired(account.Setup2FA())) mux.Handle("/account/delete", middleware.LoginRequired(account.Delete())) + mux.Handle("/account/deactivate", middleware.LoginRequired(account.Deactivate())) + mux.Handle("/account/reactivate", middleware.LoginRequired(account.Reactivate())) mux.Handle("/u/", account.Profile()) // public access OK mux.Handle("/photo/upload", middleware.LoginRequired(photo.Upload())) mux.Handle("/photo/u/", middleware.LoginRequired(photo.UserPhotos())) @@ -63,6 +65,7 @@ func New() http.Handler { mux.Handle("/messages/delete", middleware.LoginRequired(inbox.Delete())) mux.Handle("/friends", middleware.LoginRequired(friend.Friends())) mux.Handle("/friends/add", middleware.LoginRequired(friend.AddFriend())) + mux.Handle("/friends/u/", middleware.CertRequired(account.UserFriends())) mux.Handle("/users/block", middleware.LoginRequired(block.BlockUser())) mux.Handle("/users/blocked", middleware.LoginRequired(block.Blocked())) mux.Handle("/users/blocklist/add", middleware.LoginRequired(block.AddUser())) diff --git a/pkg/templates/template_vars.go b/pkg/templates/template_vars.go index f3f829c..fe552d7 100644 --- a/pkg/templates/template_vars.go +++ b/pkg/templates/template_vars.go @@ -77,7 +77,7 @@ func MergeUserVars(r *http.Request, m map[string]interface{}) { ) // Get unread message count. - if count, err := models.CountUnreadMessages(user.ID); err == nil { + if count, err := models.CountUnreadMessages(user); err == nil { m["NavUnreadMessages"] = count countMessages = count } else { @@ -93,7 +93,7 @@ func MergeUserVars(r *http.Request, m map[string]interface{}) { } // Count other notifications. - if count, err := models.CountUnreadNotifications(user.ID); err == nil { + if count, err := models.CountUnreadNotifications(user); err == nil { m["NavUnreadNotifications"] = count countNotifications = count } else { diff --git a/web/templates/account/dashboard.html b/web/templates/account/dashboard.html index 506c0d9..2b01af6 100644 --- a/web/templates/account/dashboard.html +++ b/web/templates/account/dashboard.html @@ -236,7 +236,7 @@ {{end}}
  • - + Delete account diff --git a/web/templates/account/deactivate.html b/web/templates/account/deactivate.html new file mode 100644 index 0000000..8b74f49 --- /dev/null +++ b/web/templates/account/deactivate.html @@ -0,0 +1,86 @@ +{{define "title"}}Deactivate Account{{end}} +{{define "content"}} +
    +
    +
    +
    +

    + Deactivate Account +

    +
    +
    +
    + +
    +
    +
    +
    +
    +

    + + Deactivate My Account +

    +
    +
    +
    + {{InputCSRF}} +
    +

    + On this page you may temporarily deactivate your account, which + will hide your profile from everywhere on the website (as if your account were + deleted), but in a recoverable way where you may log in and re-activate your + account in the future, should you decide to come back. +

    + +

    + When you deactivate your account: +

    + +
      +
    • + Your profile will be hidden from everywhere on the website: for example + you will not be searchable on the Member Directory. +
    • +
    • + People you had exchanged Messages with will no longer see your conversations + in their inbox or outbox page. +
    • +
    • + All comments and forum posts you made will be hidden from everybody on + the website. +
    • +
    • + You will be signed out of your {{PrettyTitle}} account. If you wish to + re-activate your account in the future, you may sign back in and the only + options you will be given will be to re-activate your account, delete it + permanently, or log out. +
    • +
    + +

    + To confirm deactivation of your account, please enter your current account + password into the box below. +

    +
    + +
    + + +
    + +
    + + Cancel +
    +
    +
    +
    +
    +
    +
    + +
    +{{end}} diff --git a/web/templates/account/friends.html b/web/templates/account/friends.html new file mode 100644 index 0000000..d1614e3 --- /dev/null +++ b/web/templates/account/friends.html @@ -0,0 +1,159 @@ +{{define "title"}}Friends of {{.User.Username}}{{end}} +{{define "content"}} +
    + {{$Root := .}} + + + + +
    + +
    + Found {{.Pager.Total}} friend{{Pluralize64 .Pager.Total}} + (page {{.Pager.Page}} of {{.Pager.Pages}}). +
    + +
    + {{SimplePager .Pager}} +
    + +
    + + {{range .Friends}} +
    + +
    + {{InputCSRF}} + + +
    +
    +
    +
    + {{template "avatar-64x64" .}} + + + {{if $Root.FriendMap.Get .ID}} +
    + + + Friends + +
    + {{end}} +
    +
    +

    + + {{if ne .Status "active"}} + {{.NameOrUsername}} + {{else}} + {{.NameOrUsername}} + {{end}} + {{if and $Root.CurrentUser.IsInnerCircle .InnerCircle}} + + {{end}} + + {{if eq .Visibility "private"}} + + {{end}} +

    +

    + + {{.Username}} +

    + {{if .GetProfileField "city"}} +

    + {{.GetProfileField "city"}} +

    + {{end}} +

    + {{if or (ne .GetDisplayAge "n/a")}} + {{.GetDisplayAge}} + {{end}} + + {{if .GetProfileField "gender"}} + {{.GetProfileField "gender"}} + {{end}} + + {{if .GetProfileField "pronouns"}} + {{.GetProfileField "pronouns"}} + {{end}} + + {{if .GetProfileField "orientation"}} + {{.GetProfileField "orientation"}} + {{end}} +

    +
    +
    +
    +
    + +
    + +
    + {{end}} +
    + +
    + {{SimplePager .Pager}} +
    + +
    +
    +{{end}} diff --git a/web/templates/account/profile.html b/web/templates/account/profile.html index 6d472ec..9c3758b 100644 --- a/web/templates/account/profile.html +++ b/web/templates/account/profile.html @@ -295,6 +295,17 @@
  • +
  • + + + + + + Friends + {{if .FriendCount}}{{.FriendCount}}{{end}} + + +
  • diff --git a/web/templates/account/settings.html b/web/templates/account/settings.html index 03b06d8..2939e4b 100644 --- a/web/templates/account/settings.html +++ b/web/templates/account/settings.html @@ -65,7 +65,16 @@ Account Settings

    - Change password or e-mail; Two-factor auth (2FA); delete account. + Change password or e-mail; set up Two-Factor Authentication (2FA). +

    +
    + + +
  • + + Deactivate Account +

    + Temporarily deactivate or permanently delete my account.

  • @@ -744,13 +753,41 @@ + + + +
    +
    +
    +

    + + Deactivate My Account +

    +
    + +
    +

    + If you'd like to take a break from {{PrettyTitle}} but think you may want to + come back later, you may temporarily deactivate your account + which will mark your profile as hidden from everywhere on the website (as if + it were deleted), but in a way that you can recover your account and reactivate + it again in the future. +

    + +

    + + Temporarily Deactivate My Account + +

    +
    +

    - Delete Account + Permanently Delete My Account

    @@ -762,7 +799,7 @@

    - Delete My Account + Permanently Delete My Account

    @@ -782,7 +819,8 @@ window.addEventListener("DOMContentLoaded", (event) => { $prefs = document.querySelector("#prefs"), $location = document.querySelector("#location"), $privacy = document.querySelector("#privacy"), - $account = document.querySelector("#account"), + $account = document.querySelector("#account") + $deactivate = document.querySelector("#deactivate"), buttons = Array.from(document.getElementsByClassName("nonshy-tab-button")); // Hide all by default. @@ -791,6 +829,7 @@ window.addEventListener("DOMContentLoaded", (event) => { $location.style.display = 'none'; $privacy.style.display = 'none'; $account.style.display = 'none'; + $deactivate.style.display = 'none'; // Current tab to select by default. let $activeTab = $profile; @@ -813,6 +852,9 @@ window.addEventListener("DOMContentLoaded", (event) => { case "account": $activeTab = $account; break; + case "deactivate": + $activeTab = $deactivate; + break; default: $activeTab = $profile; } diff --git a/web/templates/account/user_notes.html b/web/templates/account/user_notes.html index d65f71e..c4d60f7 100644 --- a/web/templates/account/user_notes.html +++ b/web/templates/account/user_notes.html @@ -55,6 +55,17 @@ +
  • + + + + + + Friends + {{if .FriendCount}}{{.FriendCount}}{{end}} + + +
  • diff --git a/web/templates/errors/disabled_account.html b/web/templates/errors/disabled_account.html new file mode 100644 index 0000000..0531b12 --- /dev/null +++ b/web/templates/errors/disabled_account.html @@ -0,0 +1,57 @@ +{{define "title"}}Reactivate Your Account{{end}} +{{define "content"}} +
    + + +
    +

    Your account is currently deactivated

    + +

    + Welcome back to {{PrettyTitle}}! You had requested to deactivate your account before, and you must make + a decision as to how you want to proceed from here. +

    + +

    Reactivate Your Account?

    + +

    + If you wish to pick up where you left off, you may reactivate your account by clicking + on the button below. It will be as if you never left: your profile, photo gallery, friends, messages and + notifications will all be how you left them. +

    + +

    + Reactivate My Account +

    + +

    Permanently Delete Your Account?

    + +

    + If you've decided you really don't want to have a {{PrettyTitle}} account anymore, you may + click the button below to permanently delete your account instead. We will delete your + profile, photos, and all the associated data we have about your account. This will be an irreversible + process! +

    + +

    + Permanently Delete My Account +

    + +

    Log Out

    + +

    + If you're not sure what to do, you may log out of your account and come back later. +

    + +

    + Log out of my account +

    +
    +
    +{{end}} diff --git a/web/templates/forum/newest.html b/web/templates/forum/newest.html index d2a3575..ab85f02 100644 --- a/web/templates/forum/newest.html +++ b/web/templates/forum/newest.html @@ -51,7 +51,7 @@ {{template "avatar-96x96" $User}}
    - {{$User.Username}} + {{or $User.Username "[unavailable]"}}
    diff --git a/web/templates/photo/gallery.html b/web/templates/photo/gallery.html index 15b1b89..9d2ab01 100644 --- a/web/templates/photo/gallery.html +++ b/web/templates/photo/gallery.html @@ -145,6 +145,17 @@ +
  • + + + + + + Friends + {{if .FriendCount}}{{.FriendCount}}{{end}} + + +
  • {{end}}