FossilRepo

Fix redirect loop: trust ALB X-Forwarded-Proto header

lmata 2026-04-07 04:23 trunk
Commit bc56b37e98957aeff195bdc1ca45d052511a4a488fc524b15ec9f98e3ca6d61b
--- config/settings.py
+++ config/settings.py
@@ -140,10 +140,11 @@
140140
141141
if not DEBUG:
142142
SESSION_COOKIE_SECURE = True
143143
CSRF_COOKIE_SECURE = True
144144
SECURE_SSL_REDIRECT = True
145
+ SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
145146
146147
# --- i18n ---
147148
148149
LANGUAGE_CODE = "en-us"
149150
TIME_ZONE = "UTC"
150151
--- config/settings.py
+++ config/settings.py
@@ -140,10 +140,11 @@
140
141 if not DEBUG:
142 SESSION_COOKIE_SECURE = True
143 CSRF_COOKIE_SECURE = True
144 SECURE_SSL_REDIRECT = True
 
145
146 # --- i18n ---
147
148 LANGUAGE_CODE = "en-us"
149 TIME_ZONE = "UTC"
150
--- config/settings.py
+++ config/settings.py
@@ -140,10 +140,11 @@
140
141 if not DEBUG:
142 SESSION_COOKIE_SECURE = True
143 CSRF_COOKIE_SECURE = True
144 SECURE_SSL_REDIRECT = True
145 SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
146
147 # --- i18n ---
148
149 LANGUAGE_CODE = "en-us"
150 TIME_ZONE = "UTC"
151
--- fossil/models.py
+++ fossil/models.py
@@ -64,7 +64,8 @@
6464
6565
def __str__(self):
6666
return f"{self.repository.filename} @ {self.created_at:%Y-%m-%d %H:%M}" if self.created_at else self.repository.filename
6767
6868
69
-# Import sync models so they're discoverable by Django
69
+# Import related models so they're discoverable by Django
70
+from fossil.notifications import Notification, ProjectWatch # noqa: E402, F401
7071
from fossil.sync_models import GitMirror, SSHKey, SyncLog # noqa: E402, F401
7172
7273
ADDED fossil/notifications.py
--- fossil/models.py
+++ fossil/models.py
@@ -64,7 +64,8 @@
64
65 def __str__(self):
66 return f"{self.repository.filename} @ {self.created_at:%Y-%m-%d %H:%M}" if self.created_at else self.repository.filename
67
68
69 # Import sync models so they're discoverable by Django
 
70 from fossil.sync_models import GitMirror, SSHKey, SyncLog # noqa: E402, F401
71
72 DDED fossil/notifications.py
--- fossil/models.py
+++ fossil/models.py
@@ -64,7 +64,8 @@
64
65 def __str__(self):
66 return f"{self.repository.filename} @ {self.created_at:%Y-%m-%d %H:%M}" if self.created_at else self.repository.filename
67
68
69 # Import related models so they're discoverable by Django
70 from fossil.notifications import Notification, ProjectWatch # noqa: E402, F401
71 from fossil.sync_models import GitMirror, SSHKey, SyncLog # noqa: E402, F401
72
73 DDED fossil/notifications.py
--- a/fossil/notifications.py
+++ b/fossil/notifications.py
@@ -0,0 +1,62 @@
1
+"""Notification system for Fossilrepo.
2
+
3
+Simple SMTP-based notifications for self-hosted deployments.
4
+Users watch projects and get emails on checkins, tickets, wiki, forum changes.
5
+"""
6
+
7
+import logging
8
+
9
+from django.conf import settings
10
+from django.contrib.auth.models import User
11
+from django.core.mail import send_mail
12
+from django.db import models
13
+
14
+from core.models import ActiveManager, Tracking
15
+
16
+logger = logging.getLogger(__name__)
17
+
18
+
19
+class ProjectWatch(Tracking):
20
+ """User's subscription to project notifications."""
21
+
22
+ class EventType(models.TextChoices):
23
+ ALL = "all", "All Events"
24
+ CHECKINS = "checkins", "Checkins Only"
25
+ TICKETS = "tickets", "Tickets Only"
26
+ WIKI = "wiki", "Wiki Only"
27
+
28
+ user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="project_watches")
29
+ project = models.ForeignKey("projects.Project", on_delete=models.CASCADE, related_name="watchers")
30
+ event_filter = models.CharField(max_length=20, choices=EventType.choices, default=EventType.ALL)
31
+ email_enabled = models.BooleanField(default=True)
32
+
33
+ objects = ActiveManager()
34
+ all_objects = models.Manager()
35
+
36
+ class Meta:
37
+ unique_together = ("user", "project")
38
+
39
+ def __str__(self):
40
+ return f"{self.user.username} watching {self.project.name}"
41
+
42
+
43
+class Notification(models.Model):
44
+ """Individual notification entry."""
45
+
46
+ user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="notifications")
47
+ project = models.ForeignKey("projects.Project", on_delete=models.CASCADE, related_name="notifications")
48
+ event_type = models.CharField(max_length=20) # checkin, ticket, wiki, forum
49
+ title = models.CharField(max_length=300)
50
+ body = models.TextField(blank=True, default="")
51
+ url = models.CharField(max_length=500, blank=True, default="")
52
+ read = models.BooleanField(default=False)
53
+ emailed = models.BooleanField(default=False)
54
+ created_at = models.DateTimeField(auto_now_add=True)
55
+
56
+ class Meta:
57
+ ordering = ["-created_at"]
58
+
59
+ def __str__(self):
60
+ return f"{self.title} �ion entry."""
61
+
62
+ user = models.ForeignKey(User, on_delete=models
--- a/fossil/notifications.py
+++ b/fossil/notifications.py
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/fossil/notifications.py
+++ b/fossil/notifications.py
@@ -0,0 +1,62 @@
1 """Notification system for Fossilrepo.
2
3 Simple SMTP-based notifications for self-hosted deployments.
4 Users watch projects and get emails on checkins, tickets, wiki, forum changes.
5 """
6
7 import logging
8
9 from django.conf import settings
10 from django.contrib.auth.models import User
11 from django.core.mail import send_mail
12 from django.db import models
13
14 from core.models import ActiveManager, Tracking
15
16 logger = logging.getLogger(__name__)
17
18
19 class ProjectWatch(Tracking):
20 """User's subscription to project notifications."""
21
22 class EventType(models.TextChoices):
23 ALL = "all", "All Events"
24 CHECKINS = "checkins", "Checkins Only"
25 TICKETS = "tickets", "Tickets Only"
26 WIKI = "wiki", "Wiki Only"
27
28 user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="project_watches")
29 project = models.ForeignKey("projects.Project", on_delete=models.CASCADE, related_name="watchers")
30 event_filter = models.CharField(max_length=20, choices=EventType.choices, default=EventType.ALL)
31 email_enabled = models.BooleanField(default=True)
32
33 objects = ActiveManager()
34 all_objects = models.Manager()
35
36 class Meta:
37 unique_together = ("user", "project")
38
39 def __str__(self):
40 return f"{self.user.username} watching {self.project.name}"
41
42
43 class Notification(models.Model):
44 """Individual notification entry."""
45
46 user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="notifications")
47 project = models.ForeignKey("projects.Project", on_delete=models.CASCADE, related_name="notifications")
48 event_type = models.CharField(max_length=20) # checkin, ticket, wiki, forum
49 title = models.CharField(max_length=300)
50 body = models.TextField(blank=True, default="")
51 url = models.CharField(max_length=500, blank=True, default="")
52 read = models.BooleanField(default=False)
53 emailed = models.BooleanField(default=False)
54 created_at = models.DateTimeField(auto_now_add=True)
55
56 class Meta:
57 ordering = ["-created_at"]
58
59 def __str__(self):
60 return f"{self.title} �ion entry."""
61
62 user = models.ForeignKey(User, on_delete=models
--- fossil/urls.py
+++ fossil/urls.py
@@ -36,10 +36,11 @@
3636
path("sync/git/callback/github/", views.oauth_github_callback, name="oauth_github_callback"),
3737
path("sync/git/callback/gitlab/", views.oauth_gitlab_callback, name="oauth_gitlab_callback"),
3838
path("code/raw/<path:filepath>", views.code_raw, name="code_raw"),
3939
path("code/blame/<path:filepath>", views.code_blame, name="code_blame"),
4040
path("code/history/<path:filepath>", views.file_history, name="file_history"),
41
+ path("watch/", views.toggle_watch, name="toggle_watch"),
4142
path("timeline/rss/", views.timeline_rss, name="timeline_rss"),
4243
path("tickets/export/", views.tickets_csv, name="tickets_csv"),
4344
path("docs/", views.fossil_docs, name="docs"),
4445
path("docs/<path:doc_path>", views.fossil_doc_page, name="doc_page"),
4546
]
4647
--- fossil/urls.py
+++ fossil/urls.py
@@ -36,10 +36,11 @@
36 path("sync/git/callback/github/", views.oauth_github_callback, name="oauth_github_callback"),
37 path("sync/git/callback/gitlab/", views.oauth_gitlab_callback, name="oauth_gitlab_callback"),
38 path("code/raw/<path:filepath>", views.code_raw, name="code_raw"),
39 path("code/blame/<path:filepath>", views.code_blame, name="code_blame"),
40 path("code/history/<path:filepath>", views.file_history, name="file_history"),
 
41 path("timeline/rss/", views.timeline_rss, name="timeline_rss"),
42 path("tickets/export/", views.tickets_csv, name="tickets_csv"),
43 path("docs/", views.fossil_docs, name="docs"),
44 path("docs/<path:doc_path>", views.fossil_doc_page, name="doc_page"),
45 ]
46
--- fossil/urls.py
+++ fossil/urls.py
@@ -36,10 +36,11 @@
36 path("sync/git/callback/github/", views.oauth_github_callback, name="oauth_github_callback"),
37 path("sync/git/callback/gitlab/", views.oauth_gitlab_callback, name="oauth_gitlab_callback"),
38 path("code/raw/<path:filepath>", views.code_raw, name="code_raw"),
39 path("code/blame/<path:filepath>", views.code_blame, name="code_blame"),
40 path("code/history/<path:filepath>", views.file_history, name="file_history"),
41 path("watch/", views.toggle_watch, name="toggle_watch"),
42 path("timeline/rss/", views.timeline_rss, name="timeline_rss"),
43 path("tickets/export/", views.tickets_csv, name="tickets_csv"),
44 path("docs/", views.fossil_docs, name="docs"),
45 path("docs/<path:doc_path>", views.fossil_doc_page, name="doc_page"),
46 ]
47

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button