Add secret management guide, client-side performance monitoring, and search accessibility enhancements

- Introduced a comprehensive Secret Management Guide detailing best practices, secret classification, development setup, production management, rotation procedures, and emergency protocols.
- Implemented a client-side performance monitoring script to track various metrics including page load performance, paint metrics, layout shifts, and memory usage.
- Enhanced search accessibility with keyboard navigation support for search results, ensuring compliance with WCAG standards and improving user experience.
This commit is contained in:
pacnpal
2025-12-23 16:41:42 -05:00
parent ae31e889d7
commit edcd8f2076
155 changed files with 22046 additions and 4645 deletions

View File

@@ -50,21 +50,31 @@ class User(AbstractUser):
max_length=10,
default="USER",
db_index=True,
help_text="User role (user, moderator, admin)",
)
is_banned = models.BooleanField(
default=False, db_index=True, help_text="Whether this user is banned"
)
ban_reason = models.TextField(blank=True, help_text="Reason for ban")
ban_date = models.DateTimeField(
null=True, blank=True, help_text="Date the user was banned"
)
is_banned = models.BooleanField(default=False, db_index=True)
ban_reason = models.TextField(blank=True)
ban_date = models.DateTimeField(null=True, blank=True)
pending_email = models.EmailField(blank=True, null=True)
theme_preference = RichChoiceField(
choice_group="theme_preferences",
domain="accounts",
max_length=5,
default="light",
help_text="User's theme preference (light/dark)",
)
# Notification preferences
email_notifications = models.BooleanField(default=True)
push_notifications = models.BooleanField(default=False)
email_notifications = models.BooleanField(
default=True, help_text="Whether to send email notifications"
)
push_notifications = models.BooleanField(
default=False, help_text="Whether to send push notifications"
)
# Privacy settings
privacy_level = RichChoiceField(
@@ -72,31 +82,65 @@ class User(AbstractUser):
domain="accounts",
max_length=10,
default="public",
help_text="Overall privacy level",
)
show_email = models.BooleanField(
default=False, help_text="Whether to show email on profile"
)
show_real_name = models.BooleanField(
default=True, help_text="Whether to show real name on profile"
)
show_join_date = models.BooleanField(
default=True, help_text="Whether to show join date on profile"
)
show_statistics = models.BooleanField(
default=True, help_text="Whether to show statistics on profile"
)
show_reviews = models.BooleanField(
default=True, help_text="Whether to show reviews on profile"
)
show_photos = models.BooleanField(
default=True, help_text="Whether to show photos on profile"
)
show_top_lists = models.BooleanField(
default=True, help_text="Whether to show top lists on profile"
)
allow_friend_requests = models.BooleanField(
default=True, help_text="Whether to allow friend requests"
)
allow_messages = models.BooleanField(
default=True, help_text="Whether to allow direct messages"
)
allow_profile_comments = models.BooleanField(
default=False, help_text="Whether to allow profile comments"
)
search_visibility = models.BooleanField(
default=True, help_text="Whether profile appears in search results"
)
show_email = models.BooleanField(default=False)
show_real_name = models.BooleanField(default=True)
show_join_date = models.BooleanField(default=True)
show_statistics = models.BooleanField(default=True)
show_reviews = models.BooleanField(default=True)
show_photos = models.BooleanField(default=True)
show_top_lists = models.BooleanField(default=True)
allow_friend_requests = models.BooleanField(default=True)
allow_messages = models.BooleanField(default=True)
allow_profile_comments = models.BooleanField(default=False)
search_visibility = models.BooleanField(default=True)
activity_visibility = RichChoiceField(
choice_group="privacy_levels",
domain="accounts",
max_length=10,
default="friends",
help_text="Who can see user activity",
)
# Security settings
two_factor_enabled = models.BooleanField(default=False)
login_notifications = models.BooleanField(default=True)
session_timeout = models.IntegerField(default=30) # days
login_history_retention = models.IntegerField(default=90) # days
last_password_change = models.DateTimeField(auto_now_add=True)
two_factor_enabled = models.BooleanField(
default=False, help_text="Whether two-factor authentication is enabled"
)
login_notifications = models.BooleanField(
default=True, help_text="Whether to send login notifications"
)
session_timeout = models.IntegerField(
default=30, help_text="Session timeout in days"
)
login_history_retention = models.IntegerField(
default=90, help_text="How long to retain login history (days)"
)
last_password_change = models.DateTimeField(
auto_now_add=True, help_text="When the password was last changed"
)
# Display name - core user data for better performance
display_name = models.CharField(
@@ -129,6 +173,8 @@ class User(AbstractUser):
return self.username
class Meta:
verbose_name = "User"
verbose_name_plural = "Users"
indexes = [
models.Index(fields=['is_banned', 'role'], name='accounts_user_banned_role_idx'),
]
@@ -156,7 +202,12 @@ class UserProfile(models.Model):
help_text="Unique identifier for this profile that remains constant",
)
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="profile")
user = models.OneToOneField(
User,
on_delete=models.CASCADE,
related_name="profile",
help_text="User this profile belongs to",
)
display_name = models.CharField(
max_length=50,
blank=True,
@@ -166,23 +217,34 @@ class UserProfile(models.Model):
'django_cloudflareimages_toolkit.CloudflareImage',
on_delete=models.SET_NULL,
null=True,
blank=True
blank=True,
help_text="User's avatar image",
)
pronouns = models.CharField(
max_length=50, blank=True, help_text="User's preferred pronouns"
)
pronouns = models.CharField(max_length=50, blank=True)
bio = models.TextField(max_length=500, blank=True)
bio = models.TextField(max_length=500, blank=True, help_text="User biography")
# Social media links
twitter = models.URLField(blank=True)
instagram = models.URLField(blank=True)
youtube = models.URLField(blank=True)
discord = models.CharField(max_length=100, blank=True)
twitter = models.URLField(blank=True, help_text="Twitter profile URL")
instagram = models.URLField(blank=True, help_text="Instagram profile URL")
youtube = models.URLField(blank=True, help_text="YouTube channel URL")
discord = models.CharField(max_length=100, blank=True, help_text="Discord username")
# Ride statistics
coaster_credits = models.IntegerField(default=0)
dark_ride_credits = models.IntegerField(default=0)
flat_ride_credits = models.IntegerField(default=0)
water_ride_credits = models.IntegerField(default=0)
coaster_credits = models.IntegerField(
default=0, help_text="Number of roller coasters ridden"
)
dark_ride_credits = models.IntegerField(
default=0, help_text="Number of dark rides ridden"
)
flat_ride_credits = models.IntegerField(
default=0, help_text="Number of flat rides ridden"
)
water_ride_credits = models.IntegerField(
default=0, help_text="Number of water rides ridden"
)
def get_avatar_url(self):
"""
@@ -265,13 +327,28 @@ class UserProfile(models.Model):
def __str__(self):
return self.display_name
class Meta:
verbose_name = "User Profile"
verbose_name_plural = "User Profiles"
ordering = ["user"]
@pghistory.track()
class EmailVerification(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
token = models.CharField(max_length=64, unique=True)
created_at = models.DateTimeField(auto_now_add=True)
last_sent = models.DateTimeField(auto_now_add=True)
user = models.OneToOneField(
User,
on_delete=models.CASCADE,
help_text="User this verification belongs to",
)
token = models.CharField(
max_length=64, unique=True, help_text="Verification token"
)
created_at = models.DateTimeField(
auto_now_add=True, help_text="When this verification was created"
)
last_sent = models.DateTimeField(
auto_now_add=True, help_text="When the verification email was last sent"
)
def __str__(self):
return f"Email verification for {self.user.username}"
@@ -283,11 +360,17 @@ class EmailVerification(models.Model):
@pghistory.track()
class PasswordReset(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
token = models.CharField(max_length=64)
created_at = models.DateTimeField(auto_now_add=True)
expires_at = models.DateTimeField()
used = models.BooleanField(default=False)
user = models.ForeignKey(
User,
on_delete=models.CASCADE,
help_text="User requesting password reset",
)
token = models.CharField(max_length=64, help_text="Reset token")
created_at = models.DateTimeField(
auto_now_add=True, help_text="When this reset was requested"
)
expires_at = models.DateTimeField(help_text="When this reset token expires")
used = models.BooleanField(default=False, help_text="Whether this token has been used")
def __str__(self):
return f"Password reset for {self.user.username}"
@@ -304,19 +387,23 @@ class TopList(TrackedModel):
user = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name="top_lists", # Added related_name for User model access
related_name="top_lists",
help_text="User who created this list",
)
title = models.CharField(max_length=100)
title = models.CharField(max_length=100, help_text="Title of the top list")
category = RichChoiceField(
choice_group="top_list_categories",
domain="accounts",
max_length=2,
help_text="Category of items in this list",
)
description = models.TextField(blank=True)
description = models.TextField(blank=True, help_text="Description of the list")
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta(TrackedModel.Meta):
verbose_name = "Top List"
verbose_name_plural = "Top Lists"
ordering = ["-updated_at"]
def __str__(self):
@@ -330,16 +417,23 @@ class TopList(TrackedModel):
class TopListItem(TrackedModel):
top_list = models.ForeignKey(
TopList, on_delete=models.CASCADE, related_name="items"
TopList,
on_delete=models.CASCADE,
related_name="items",
help_text="Top list this item belongs to",
)
content_type = models.ForeignKey(
"contenttypes.ContentType", on_delete=models.CASCADE
"contenttypes.ContentType",
on_delete=models.CASCADE,
help_text="Type of item (park, ride, etc.)",
)
object_id = models.PositiveIntegerField()
rank = models.PositiveIntegerField()
notes = models.TextField(blank=True)
object_id = models.PositiveIntegerField(help_text="ID of the item")
rank = models.PositiveIntegerField(help_text="Position in the list")
notes = models.TextField(blank=True, help_text="User's notes about this item")
class Meta(TrackedModel.Meta):
verbose_name = "Top List Item"
verbose_name_plural = "Top List Items"
ordering = ["rank"]
unique_together = [["top_list", "rank"]]
@@ -387,6 +481,8 @@ class UserDeletionRequest(models.Model):
)
class Meta:
verbose_name = "User Deletion Request"
verbose_name_plural = "User Deletion Requests"
ordering = ["-created_at"]
indexes = [
models.Index(fields=["verification_code"]),
@@ -464,7 +560,10 @@ class UserNotification(TrackedModel):
# Core fields
user = models.ForeignKey(
User, on_delete=models.CASCADE, related_name="notifications"
User,
on_delete=models.CASCADE,
related_name="notifications",
help_text="User this notification is for",
)
notification_type = RichChoiceField(
@@ -473,14 +572,20 @@ class UserNotification(TrackedModel):
max_length=30,
)
title = models.CharField(max_length=200)
message = models.TextField()
title = models.CharField(max_length=200, help_text="Notification title")
message = models.TextField(help_text="Notification message")
# Optional related object (submission, review, etc.)
content_type = models.ForeignKey(
"contenttypes.ContentType", on_delete=models.CASCADE, null=True, blank=True
"contenttypes.ContentType",
on_delete=models.CASCADE,
null=True,
blank=True,
help_text="Type of related object",
)
object_id = models.PositiveIntegerField(
null=True, blank=True, help_text="ID of related object"
)
object_id = models.PositiveIntegerField(null=True, blank=True)
related_object = GenericForeignKey("content_type", "object_id")
# Metadata
@@ -492,14 +597,24 @@ class UserNotification(TrackedModel):
)
# Status tracking
is_read = models.BooleanField(default=False)
read_at = models.DateTimeField(null=True, blank=True)
is_read = models.BooleanField(
default=False, help_text="Whether this notification has been read"
)
read_at = models.DateTimeField(
null=True, blank=True, help_text="When this notification was read"
)
# Delivery tracking
email_sent = models.BooleanField(default=False)
email_sent_at = models.DateTimeField(null=True, blank=True)
push_sent = models.BooleanField(default=False)
push_sent_at = models.DateTimeField(null=True, blank=True)
email_sent = models.BooleanField(default=False, help_text="Whether email was sent")
email_sent_at = models.DateTimeField(
null=True, blank=True, help_text="When email was sent"
)
push_sent = models.BooleanField(
default=False, help_text="Whether push notification was sent"
)
push_sent_at = models.DateTimeField(
null=True, blank=True, help_text="When push notification was sent"
)
# Additional data (JSON field for flexibility)
extra_data = models.JSONField(default=dict, blank=True)
@@ -509,6 +624,8 @@ class UserNotification(TrackedModel):
expires_at = models.DateTimeField(null=True, blank=True)
class Meta(TrackedModel.Meta):
verbose_name = "User Notification"
verbose_name_plural = "User Notifications"
ordering = ["-created_at"]
indexes = [
models.Index(fields=["user", "is_read"]),
@@ -559,7 +676,10 @@ class NotificationPreference(TrackedModel):
"""
user = models.OneToOneField(
User, on_delete=models.CASCADE, related_name="notification_preference"
User,
on_delete=models.CASCADE,
related_name="notification_preference",
help_text="User these preferences belong to",
)
# Submission notifications