summaryrefslogtreecommitdiff
path: root/shrine/services/council.go
diff options
context:
space:
mode:
Diffstat (limited to 'shrine/services/council.go')
-rw-r--r--shrine/services/council.go297
1 files changed, 297 insertions, 0 deletions
diff --git a/shrine/services/council.go b/shrine/services/council.go
new file mode 100644
index 0000000..c3b4d6b
--- /dev/null
+++ b/shrine/services/council.go
@@ -0,0 +1,297 @@
+package services
+
+import (
+ "fmt"
+ "shrine/enums"
+ "shrine/messages"
+ "shrine/models"
+ "shrine/repositories"
+ "shrine/types/audit"
+ "shrine/types/council"
+ "shrine/types/hypertext"
+ "shrine/types/user"
+ "shrine/utils/auth"
+ "shrine/utils/meta"
+ "shrine/utils/crypto"
+ "shrine/utils/emails"
+ "shrine/utils/logger"
+ "shrine/utils/sanitize"
+ "shrine/utils/validators"
+ "strings"
+ "time"
+)
+
+func ListUsers(pagination meta.Pagination, search string) ([]user.AdminUserResponse, int64) {
+ citizens, total := repositories.ListUsers(pagination, search)
+
+ items := make([]user.AdminUserResponse, len(citizens))
+ for index, citizen := range citizens {
+ items[index] = citizen.ToAdminResponse()
+ }
+
+ return items, total
+}
+
+func GetUser(username string) (*user.AdminUserResponse, *hypertext.ServiceError) {
+ citizen, err := repositories.FindUserByUsername(username)
+ if err != nil {
+ return nil, fail(enums.NotFound, messages.UserNotFound)
+ }
+
+ response := citizen.ToAdminResponse()
+ return &response, nil
+}
+
+func BanUser(admin *models.User, target *models.User, request council.BanRequest) (*user.AdminUserResponse, *hypertext.ServiceError) {
+ if hierarchyErr := auth.ValidateHierarchy(admin, target, "ban"); hierarchyErr != nil {
+ return nil, fail(enums.Forbidden, hierarchyErr.Error())
+ }
+
+ ref := crypto.SystemRef()
+ sanitizedMessage := sanitize.HTML(request.Message)
+
+ now := time.Now()
+ target.AccountBanned = true
+ target.BannedAt = &now
+ target.BannedReason = request.Reason
+ target.BannedBy = &admin.ID
+
+ if err := repositories.UpdateUser(target); err != nil {
+ return nil, fail(enums.Internal, messages.FailedBanUser)
+ }
+
+ if sanitizedMessage != "" {
+ repositories.CreateSystemLetter(target.ID, fmt.Sprintf("Account Banned [%s]", ref), sanitizedMessage, ref)
+ }
+
+ repositories.LogAction(admin.ID, "user.ban", "user", target.Username, fmt.Sprintf("Banned user @%s", target.Username), audit.BanDetails{
+ Reason: request.Reason,
+ SystemRef: ref,
+ })
+
+ if err := emails.SendBannedNotification(target.Email, target.Username, request.Reason); err != nil {
+ logger.Errorf("Council", "Failed to send ban notification to %s: %v", target.Email, err)
+ }
+
+ response := target.ToAdminResponse()
+ return &response, nil
+}
+
+func UnbanUser(admin *models.User, target *models.User) (*user.AdminUserResponse, *hypertext.ServiceError) {
+ target.AccountBanned = false
+ target.BannedAt = nil
+ target.BannedReason = ""
+ target.BannedBy = nil
+
+ if err := repositories.UpdateUser(target); err != nil {
+ return nil, fail(enums.Internal, messages.FailedUnbanUser)
+ }
+
+ repositories.LogAction(admin.ID, "user.unban", "user", target.Username, fmt.Sprintf("Unbanned user @%s", target.Username), nil)
+
+ response := target.ToAdminResponse()
+ return &response, nil
+}
+
+func DisableUser(admin *models.User, target *models.User, request council.DisableRequest) (*user.AdminUserResponse, *hypertext.ServiceError) {
+ if hierarchyErr := auth.ValidateHierarchy(admin, target, "disable"); hierarchyErr != nil {
+ return nil, fail(enums.Forbidden, hierarchyErr.Error())
+ }
+
+ ref := crypto.SystemRef()
+ sanitizedMessage := sanitize.HTML(request.Message)
+
+ now := time.Now()
+ target.AccountDisabled = true
+ target.DisabledAt = &now
+ target.DisabledReason = request.Reason
+ target.DisabledBy = &admin.ID
+
+ var disabledUntilStr string
+ if request.DisabledUntil != nil {
+ parsed, err := time.Parse(time.RFC3339, *request.DisabledUntil)
+ if err != nil {
+ return nil, fail(enums.BadRequest, messages.InvalidDisabledUntil)
+ }
+ target.DisabledUntil = &parsed
+ disabledUntilStr = parsed.Format("January 2, 2006")
+ }
+
+ if err := repositories.UpdateUser(target); err != nil {
+ return nil, fail(enums.Internal, messages.FailedDisableUser)
+ }
+
+ if sanitizedMessage != "" {
+ repositories.CreateSystemLetter(target.ID, fmt.Sprintf("Account Disabled [%s]", ref), sanitizedMessage, ref)
+ }
+
+ repositories.LogAction(admin.ID, "user.disable", "user", target.Username, fmt.Sprintf("Disabled user @%s", target.Username), audit.DisableDetails{
+ Reason: request.Reason,
+ DisabledUntil: request.DisabledUntil,
+ SystemRef: ref,
+ })
+
+ if err := emails.SendDisabledNotification(target.Email, target.Username, request.Reason, disabledUntilStr); err != nil {
+ logger.Errorf("Council", "Failed to send disable notification to %s: %v", target.Email, err)
+ }
+
+ response := target.ToAdminResponse()
+ return &response, nil
+}
+
+func EnableUser(admin *models.User, target *models.User) (*user.AdminUserResponse, *hypertext.ServiceError) {
+ target.AccountDisabled = false
+ target.DisabledAt = nil
+ target.DisabledReason = ""
+ target.DisabledBy = nil
+ target.DisabledUntil = nil
+
+ if err := repositories.UpdateUser(target); err != nil {
+ return nil, fail(enums.Internal, messages.FailedEnableUser)
+ }
+
+ repositories.LogAction(admin.ID, "user.enable", "user", target.Username, fmt.Sprintf("Enabled user @%s", target.Username), nil)
+
+ response := target.ToAdminResponse()
+ return &response, nil
+}
+
+func ChangeRole(admin *models.User, target *models.User, request council.ChangeRoleRequest) (*user.AdminUserResponse, *hypertext.ServiceError) {
+ if target.ID == admin.ID {
+ return nil, fail(enums.BadRequest, messages.CannotChangeOwnRole)
+ }
+
+ if target.IsOwner() {
+ return nil, fail(enums.BadRequest, messages.CannotChangeOwnerRole)
+ }
+
+ oldRole := string(target.Role)
+ role := enums.UserRole(request.Role)
+
+ switch role {
+ case enums.Member, enums.Moderator:
+ target.Role = role
+ case enums.Admin:
+ if !admin.IsOwner() {
+ return nil, fail(enums.Forbidden, messages.OnlyOwnerAssignAdmin)
+ }
+ target.Role = role
+ default:
+ return nil, fail(enums.BadRequest, messages.InvalidRole)
+ }
+
+ if err := repositories.UpdateUser(target); err != nil {
+ return nil, fail(enums.Internal, messages.FailedChangeRole)
+ }
+
+ repositories.LogAction(admin.ID, "user.role_change", "user", target.Username, fmt.Sprintf("Changed role of @%s from %s to %s", target.Username, oldRole, request.Role), audit.RoleChangeDetails{
+ OldRole: oldRole,
+ NewRole: request.Role,
+ })
+
+ response := target.ToAdminResponse()
+ return &response, nil
+}
+
+func EditUser(admin *models.User, target *models.User, request council.EditUserRequest) (*user.AdminUserResponse, *hypertext.ServiceError) {
+ var changes []audit.FieldChange
+
+ if request.Email != nil && !admin.IsOwner() {
+ return nil, fail(enums.Forbidden, messages.OnlyOwnerChangeEmail)
+ }
+
+ if request.Username != nil && *request.Username != target.Username {
+ username := strings.TrimSpace(*request.Username)
+ if !validators.IsValidUsername(username, 3) {
+ return nil, fail(enums.BadRequest, messages.InvalidUsername)
+ }
+ if validators.IsReservedUsername(username) {
+ return nil, fail(enums.BadRequest, messages.UsernameNotAvailable)
+ }
+ changes = append(changes, audit.FieldChange{Field: "username", Old: target.Username, New: username})
+ target.Username = username
+ }
+
+ if request.DisplayName != nil {
+ changes = append(changes, audit.FieldChange{Field: "display_name", Old: target.DisplayName, New: *request.DisplayName})
+ target.DisplayName = *request.DisplayName
+ }
+
+ if request.Email != nil {
+ changes = append(changes, audit.FieldChange{Field: "email", Old: target.Email, New: *request.Email})
+ target.Email = *request.Email
+ }
+
+ if request.Bio != nil {
+ target.Bio = *request.Bio
+ changes = append(changes, audit.FieldChange{Field: "bio", Old: nil, New: nil})
+ }
+
+ if request.Birthday != nil {
+ if *request.Birthday == "" {
+ target.Birthday = nil
+ } else {
+ parsed, err := time.Parse("2006-01-02", *request.Birthday)
+ if err != nil {
+ return nil, fail(enums.BadRequest, "Invalid birthday format. Use YYYY-MM-DD.")
+ }
+ target.Birthday = &parsed
+ }
+ changes = append(changes, audit.FieldChange{Field: "birthday", Old: nil, New: nil})
+ }
+
+ if request.AvatarURL != nil {
+ target.AvatarURL = *request.AvatarURL
+ changes = append(changes, audit.FieldChange{Field: "avatar_url", Old: nil, New: nil})
+ }
+
+ if request.BlinkieURL != nil {
+ target.BlinkieURL = *request.BlinkieURL
+ changes = append(changes, audit.FieldChange{Field: "blinkie_url", Old: nil, New: nil})
+ }
+
+ if request.Website != nil {
+ target.Website = *request.Website
+ changes = append(changes, audit.FieldChange{Field: "website", Old: nil, New: nil})
+ }
+
+ if request.Location != nil {
+ target.Location = *request.Location
+ changes = append(changes, audit.FieldChange{Field: "location", Old: nil, New: nil})
+ }
+
+ if request.Pronouns != nil {
+ target.Pronouns = *request.Pronouns
+ changes = append(changes, audit.FieldChange{Field: "pronouns", Old: nil, New: nil})
+ }
+
+ if request.Signature != nil {
+ target.Signature = *request.Signature
+ changes = append(changes, audit.FieldChange{Field: "signature", Old: nil, New: nil})
+ }
+
+ if request.Jade != nil {
+ changes = append(changes, audit.FieldChange{Field: "jade", Old: target.Jade, New: *request.Jade})
+ target.Jade = *request.Jade
+ }
+
+ if request.Honor != nil {
+ changes = append(changes, audit.FieldChange{Field: "honor", Old: target.Honor, New: *request.Honor})
+ target.Honor = *request.Honor
+ }
+
+ if len(changes) == 0 {
+ return nil, fail(enums.BadRequest, messages.NoChangesProvided)
+ }
+
+ if err := repositories.UpdateUser(target); err != nil {
+ return nil, fail(enums.Internal, messages.FailedUpdateUser)
+ }
+
+ repositories.LogAction(admin.ID, "user.edit", "user", target.Username, fmt.Sprintf("Edited user @%s", target.Username), audit.EditUserDetails{
+ Changes: changes,
+ })
+
+ response := target.ToAdminResponse()
+ return &response, nil
+} \ No newline at end of file