arthexis 0.1.8__py3-none-any.whl → 0.1.10__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of arthexis might be problematic. Click here for more details.

Files changed (84) hide show
  1. {arthexis-0.1.8.dist-info → arthexis-0.1.10.dist-info}/METADATA +87 -6
  2. arthexis-0.1.10.dist-info/RECORD +95 -0
  3. arthexis-0.1.10.dist-info/licenses/LICENSE +674 -0
  4. config/__init__.py +0 -1
  5. config/auth_app.py +0 -1
  6. config/celery.py +1 -2
  7. config/context_processors.py +1 -1
  8. config/offline.py +2 -0
  9. config/settings.py +352 -37
  10. config/urls.py +71 -6
  11. core/admin.py +1601 -200
  12. core/admin_history.py +50 -0
  13. core/admindocs.py +108 -1
  14. core/apps.py +161 -3
  15. core/auto_upgrade.py +57 -0
  16. core/backends.py +123 -7
  17. core/entity.py +62 -48
  18. core/fields.py +98 -0
  19. core/github_helper.py +25 -0
  20. core/github_issues.py +172 -0
  21. core/lcd_screen.py +1 -0
  22. core/liveupdate.py +25 -0
  23. core/log_paths.py +100 -0
  24. core/mailer.py +83 -0
  25. core/middleware.py +57 -0
  26. core/models.py +1279 -267
  27. core/notifications.py +11 -1
  28. core/public_wifi.py +227 -0
  29. core/reference_utils.py +97 -0
  30. core/release.py +27 -20
  31. core/sigil_builder.py +144 -0
  32. core/sigil_context.py +20 -0
  33. core/sigil_resolver.py +284 -0
  34. core/system.py +162 -29
  35. core/tasks.py +269 -27
  36. core/test_system_info.py +59 -1
  37. core/tests.py +644 -73
  38. core/tests_liveupdate.py +17 -0
  39. core/urls.py +2 -2
  40. core/user_data.py +425 -168
  41. core/views.py +627 -59
  42. core/widgets.py +51 -0
  43. core/workgroup_urls.py +7 -3
  44. core/workgroup_views.py +43 -6
  45. nodes/actions.py +0 -2
  46. nodes/admin.py +168 -285
  47. nodes/apps.py +9 -15
  48. nodes/backends.py +145 -0
  49. nodes/lcd.py +24 -10
  50. nodes/models.py +579 -179
  51. nodes/tasks.py +1 -5
  52. nodes/tests.py +894 -130
  53. nodes/utils.py +13 -2
  54. nodes/views.py +204 -28
  55. ocpp/admin.py +212 -63
  56. ocpp/apps.py +1 -1
  57. ocpp/consumers.py +642 -68
  58. ocpp/evcs.py +30 -10
  59. ocpp/models.py +452 -70
  60. ocpp/simulator.py +75 -11
  61. ocpp/store.py +288 -30
  62. ocpp/tasks.py +11 -7
  63. ocpp/test_export_import.py +8 -7
  64. ocpp/test_rfid.py +211 -16
  65. ocpp/tests.py +1576 -137
  66. ocpp/transactions_io.py +68 -22
  67. ocpp/urls.py +35 -2
  68. ocpp/views.py +701 -123
  69. pages/admin.py +173 -13
  70. pages/checks.py +0 -1
  71. pages/context_processors.py +39 -6
  72. pages/forms.py +131 -0
  73. pages/middleware.py +153 -0
  74. pages/models.py +37 -9
  75. pages/tests.py +1182 -42
  76. pages/urls.py +4 -0
  77. pages/utils.py +0 -1
  78. pages/views.py +844 -51
  79. arthexis-0.1.8.dist-info/RECORD +0 -80
  80. arthexis-0.1.8.dist-info/licenses/LICENSE +0 -21
  81. config/workgroup_app.py +0 -7
  82. core/checks.py +0 -29
  83. {arthexis-0.1.8.dist-info → arthexis-0.1.10.dist-info}/WHEEL +0 -0
  84. {arthexis-0.1.8.dist-info → arthexis-0.1.10.dist-info}/top_level.txt +0 -0
core/admin_history.py ADDED
@@ -0,0 +1,50 @@
1
+ import json
2
+ from django.contrib.admin.options import ModelAdmin
3
+ from django.contrib.admin.models import LogEntry
4
+ from django.utils.encoding import smart_str
5
+
6
+
7
+ def patch_admin_history():
8
+ if getattr(ModelAdmin, "_history_patched", False):
9
+ return
10
+
11
+ def construct_change_message(self, request, form, formsets, add=False):
12
+ fields = []
13
+ if add:
14
+ for name, value in form.cleaned_data.items():
15
+ fields.append({"field": name, "old": None, "new": smart_str(value)})
16
+ if not fields:
17
+ return ""
18
+ return json.dumps({"added": fields})
19
+ for name in form.changed_data:
20
+ fields.append(
21
+ {
22
+ "field": name,
23
+ "old": smart_str(form.initial.get(name)),
24
+ "new": smart_str(form.cleaned_data.get(name)),
25
+ }
26
+ )
27
+ if not fields:
28
+ return ""
29
+ return json.dumps({"changed": fields})
30
+
31
+ def get_change_message(self):
32
+ try:
33
+ data = json.loads(self.change_message)
34
+ except Exception:
35
+ return self.change_message
36
+ if isinstance(data, dict):
37
+ if "added" in data:
38
+ parts = [f"{d['field']}='{d['new']}'" for d in data["added"]]
39
+ return "Added " + ", ".join(parts)
40
+ if "changed" in data:
41
+ parts = [
42
+ f"{d['field']}: '{d['old']}' -> '{d['new']}'"
43
+ for d in data["changed"]
44
+ ]
45
+ return "Changed " + ", ".join(parts)
46
+ return self.change_message
47
+
48
+ ModelAdmin.construct_change_message = construct_change_message
49
+ LogEntry.get_change_message = get_change_message
50
+ ModelAdmin._history_patched = True
core/admindocs.py CHANGED
@@ -1,6 +1,15 @@
1
1
  import argparse
2
+ import inspect
3
+ from types import SimpleNamespace
4
+
5
+ from django.apps import apps
6
+ from django.contrib import admin
2
7
  from django.core.management import get_commands, load_command_class
3
- from django.contrib.admindocs.views import BaseAdminDocsView
8
+ from django.contrib.admindocs.views import (
9
+ BaseAdminDocsView,
10
+ user_has_model_view_permission,
11
+ )
12
+ from django.urls import NoReverseMatch, reverse
4
13
 
5
14
 
6
15
  class CommandsView(BaseAdminDocsView):
@@ -42,3 +51,101 @@ class CommandsView(BaseAdminDocsView):
42
51
  }
43
52
  )
44
53
  return super().get_context_data(**{**kwargs, "commands": commands})
54
+
55
+
56
+ class OrderedModelIndexView(BaseAdminDocsView):
57
+ template_name = "admin_doc/model_index.html"
58
+
59
+ GROUP_OVERRIDES = {
60
+ "ocpp.location": "core",
61
+ "core.rfid": "ocpp",
62
+ "core.package": "teams",
63
+ "core.packagerelease": "teams",
64
+ }
65
+
66
+ def _get_docs_app_config(self, meta):
67
+ override_label = self.GROUP_OVERRIDES.get(meta.label_lower)
68
+ if override_label:
69
+ return apps.get_app_config(override_label)
70
+ return meta.app_config
71
+
72
+ def get_context_data(self, **kwargs):
73
+ models = []
74
+ for m in apps.get_models():
75
+ if user_has_model_view_permission(self.request.user, m._meta):
76
+ meta = m._meta
77
+ meta.docstring = inspect.getdoc(m) or ""
78
+ app_config = self._get_docs_app_config(meta)
79
+ models.append(
80
+ SimpleNamespace(
81
+ app_label=meta.app_label,
82
+ model_name=meta.model_name,
83
+ object_name=meta.object_name,
84
+ docstring=meta.docstring,
85
+ app_config=app_config,
86
+ )
87
+ )
88
+ models.sort(key=lambda m: str(m.app_config.verbose_name))
89
+ return super().get_context_data(**{**kwargs, "models": models})
90
+
91
+
92
+ class ModelGraphIndexView(BaseAdminDocsView):
93
+ template_name = "admin_doc/model_graphs.html"
94
+
95
+ def get_context_data(self, **kwargs):
96
+ sections = {}
97
+ user = self.request.user
98
+
99
+ for model in admin.site._registry:
100
+ meta = model._meta
101
+ if not user_has_model_view_permission(user, meta):
102
+ continue
103
+
104
+ app_config = apps.get_app_config(meta.app_label)
105
+ section = sections.setdefault(
106
+ app_config.label,
107
+ {
108
+ "app_label": app_config.label,
109
+ "verbose_name": str(app_config.verbose_name),
110
+ "models": [],
111
+ },
112
+ )
113
+
114
+ section["models"].append(
115
+ {
116
+ "object_name": meta.object_name,
117
+ "verbose_name": str(meta.verbose_name),
118
+ "doc_url": reverse(
119
+ "django-admindocs-models-detail",
120
+ kwargs={
121
+ "app_label": meta.app_label,
122
+ "model_name": meta.model_name,
123
+ },
124
+ ),
125
+ }
126
+ )
127
+
128
+ graph_sections = []
129
+ for section in sections.values():
130
+ section_models = section["models"]
131
+ section_models.sort(key=lambda model: model["verbose_name"])
132
+
133
+ try:
134
+ app_list_url = reverse("admin:app_list", args=[section["app_label"]])
135
+ except NoReverseMatch:
136
+ app_list_url = ""
137
+
138
+ graph_sections.append(
139
+ {
140
+ **section,
141
+ "graph_url": reverse(
142
+ "admin-model-graph", args=[section["app_label"]]
143
+ ),
144
+ "app_list_url": app_list_url,
145
+ "model_count": len(section_models),
146
+ }
147
+ )
148
+
149
+ graph_sections.sort(key=lambda section: section["verbose_name"])
150
+
151
+ return super().get_context_data(**{**kwargs, "sections": graph_sections})
core/apps.py CHANGED
@@ -1,22 +1,43 @@
1
+ import logging
2
+
1
3
  from django.apps import AppConfig
2
4
  from django.utils.translation import gettext_lazy as _
3
5
 
4
6
 
7
+ logger = logging.getLogger(__name__)
8
+
9
+
5
10
  class CoreConfig(AppConfig):
6
11
  default_auto_field = "django.db.models.BigAutoField"
7
12
  name = "core"
8
13
  verbose_name = _("2. Business")
9
14
 
10
15
  def ready(self): # pragma: no cover - called by Django
16
+ from contextlib import suppress
17
+ from functools import wraps
18
+ import hashlib
19
+ import time
20
+ import traceback
21
+ from pathlib import Path
22
+
23
+ from django.conf import settings
11
24
  from django.contrib.auth import get_user_model
12
25
  from django.db.models.signals import post_migrate
26
+ from django.core.signals import got_request_exception
27
+
28
+ from core.github_helper import report_exception_to_github
29
+ from .entity import Entity
13
30
  from .user_data import (
14
31
  patch_admin_user_datum,
15
32
  patch_admin_user_data_views,
16
33
  )
17
34
  from .system import patch_admin_system_view
18
35
  from .environment import patch_admin_environment_view
19
- from . import checks # noqa: F401
36
+ from .sigil_builder import (
37
+ patch_admin_sigil_builder_view,
38
+ generate_model_sigils,
39
+ )
40
+ from .admin_history import patch_admin_history
20
41
 
21
42
  def create_default_arthexis(**kwargs):
22
43
  User = get_user_model()
@@ -29,17 +50,62 @@ class CoreConfig(AppConfig):
29
50
  )
30
51
 
31
52
  post_migrate.connect(create_default_arthexis, sender=self)
53
+ post_migrate.connect(generate_model_sigils, sender=self)
32
54
  patch_admin_user_datum()
33
55
  patch_admin_user_data_views()
34
56
  patch_admin_system_view()
35
57
  patch_admin_environment_view()
58
+ patch_admin_sigil_builder_view()
59
+ patch_admin_history()
36
60
 
37
- from pathlib import Path
38
- from django.conf import settings
61
+ from django.core.serializers import base as serializer_base
62
+
63
+ if not hasattr(
64
+ serializer_base.DeserializedObject.save, "_entity_fixture_patch"
65
+ ):
66
+ original_save = serializer_base.DeserializedObject.save
67
+
68
+ @wraps(original_save)
69
+ def patched_save(self, save_m2m=True, using=None, **kwargs):
70
+ obj = self.object
71
+ if isinstance(obj, Entity):
72
+ manager = getattr(
73
+ type(obj), "all_objects", type(obj)._default_manager
74
+ )
75
+ if using:
76
+ manager = manager.db_manager(using)
77
+ for fields in obj._unique_field_groups():
78
+ lookup = {}
79
+ for field in fields:
80
+ value = getattr(obj, field.attname)
81
+ if value is None:
82
+ lookup = {}
83
+ break
84
+ lookup[field.attname] = value
85
+ if not lookup:
86
+ continue
87
+ existing = (
88
+ manager.filter(**lookup)
89
+ .only("pk", "is_seed_data", "is_user_data")
90
+ .first()
91
+ )
92
+ if existing is not None:
93
+ obj.pk = existing.pk
94
+ obj.is_seed_data = existing.is_seed_data
95
+ obj.is_user_data = existing.is_user_data
96
+ obj._state.adding = False
97
+ if using:
98
+ obj._state.db = using
99
+ break
100
+ return original_save(self, save_m2m=save_m2m, using=using, **kwargs)
101
+
102
+ patched_save._entity_fixture_patch = True
103
+ serializer_base.DeserializedObject.save = patched_save
39
104
 
40
105
  lock = Path(settings.BASE_DIR) / "locks" / "celery.lck"
41
106
 
42
107
  if lock.exists():
108
+ from .auto_upgrade import ensure_auto_upgrade_periodic_task
43
109
 
44
110
  def ensure_email_collector_task(**kwargs):
45
111
  try: # pragma: no cover - optional dependency
@@ -66,3 +132,95 @@ class CoreConfig(AppConfig):
66
132
  pass
67
133
 
68
134
  post_migrate.connect(ensure_email_collector_task, sender=self)
135
+ post_migrate.connect(ensure_auto_upgrade_periodic_task, sender=self)
136
+ ensure_auto_upgrade_periodic_task()
137
+
138
+ from django.db.backends.signals import connection_created
139
+
140
+ def enable_sqlite_wal(**kwargs):
141
+ connection = kwargs.get("connection")
142
+ if connection.vendor == "sqlite":
143
+ cursor = connection.cursor()
144
+ cursor.execute("PRAGMA journal_mode=WAL;")
145
+ cursor.execute("PRAGMA busy_timeout=60000;")
146
+ cursor.close()
147
+
148
+ connection_created.connect(enable_sqlite_wal)
149
+
150
+ def queue_github_issue(sender, request=None, **kwargs):
151
+ if not getattr(settings, "GITHUB_ISSUE_REPORTING_ENABLED", True):
152
+ return
153
+ if request is None:
154
+ return
155
+
156
+ exception = kwargs.get("exception")
157
+ if exception is None:
158
+ return
159
+
160
+ try:
161
+ tb_exc = traceback.TracebackException.from_exception(exception)
162
+ stack = tb_exc.stack
163
+ top_frame = stack[-1] if stack else None
164
+ fingerprint_parts = [
165
+ exception.__class__.__module__,
166
+ exception.__class__.__name__,
167
+ ]
168
+ if top_frame:
169
+ fingerprint_parts.extend(
170
+ [
171
+ top_frame.filename,
172
+ str(top_frame.lineno),
173
+ top_frame.name,
174
+ ]
175
+ )
176
+ fingerprint = hashlib.sha256(
177
+ "|".join(fingerprint_parts).encode("utf-8")
178
+ ).hexdigest()
179
+
180
+ cooldown = getattr(settings, "GITHUB_ISSUE_REPORTING_COOLDOWN", 3600)
181
+ lock_dir = Path(settings.BASE_DIR) / "locks" / "github-issues"
182
+ fingerprint_path = None
183
+ now = time.time()
184
+
185
+ with suppress(OSError):
186
+ lock_dir.mkdir(parents=True, exist_ok=True)
187
+ fingerprint_path = lock_dir / fingerprint
188
+ if fingerprint_path.exists():
189
+ age = now - fingerprint_path.stat().st_mtime
190
+ if age < cooldown:
191
+ return
192
+
193
+ if fingerprint_path is not None:
194
+ with suppress(OSError):
195
+ fingerprint_path.write_text(str(now))
196
+
197
+ user_repr = None
198
+ user = getattr(request, "user", None)
199
+ if user is not None:
200
+ try:
201
+ if getattr(user, "is_authenticated", False):
202
+ user_repr = user.get_username()
203
+ else:
204
+ user_repr = "anonymous"
205
+ except Exception: # pragma: no cover - defensive
206
+ user_repr = str(user)
207
+
208
+ payload = {
209
+ "path": getattr(request, "path", None),
210
+ "method": getattr(request, "method", None),
211
+ "user": user_repr,
212
+ "active_app": getattr(request, "active_app", None),
213
+ "fingerprint": fingerprint,
214
+ "exception_class": f"{exception.__class__.__module__}.{exception.__class__.__name__}",
215
+ "traceback": "".join(tb_exc.format()),
216
+ }
217
+
218
+ report_exception_to_github.delay(payload)
219
+ except Exception: # pragma: no cover - defensive
220
+ logger.exception("Failed to queue GitHub issue from request exception")
221
+
222
+ got_request_exception.connect(
223
+ queue_github_issue,
224
+ dispatch_uid="core.github_issue_reporter",
225
+ weak=False,
226
+ )
core/auto_upgrade.py ADDED
@@ -0,0 +1,57 @@
1
+ """Helpers for managing the auto-upgrade scheduler."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from django.conf import settings
8
+
9
+
10
+ AUTO_UPGRADE_TASK_NAME = "auto-upgrade-check"
11
+ AUTO_UPGRADE_TASK_PATH = "core.tasks.check_github_updates"
12
+
13
+
14
+ def ensure_auto_upgrade_periodic_task(
15
+ sender=None, *, base_dir: Path | None = None, **kwargs
16
+ ) -> None:
17
+ """Ensure the auto-upgrade periodic task exists.
18
+
19
+ The function is signal-safe so it can be wired to Django's
20
+ ``post_migrate`` hook. When called directly the ``sender`` and
21
+ ``**kwargs`` parameters are ignored.
22
+ """
23
+
24
+ del sender, kwargs # Unused when invoked as a Django signal handler.
25
+
26
+ if base_dir is None:
27
+ base_dir = Path(settings.BASE_DIR)
28
+ else:
29
+ base_dir = Path(base_dir)
30
+
31
+ lock_dir = base_dir / "locks"
32
+ mode_file = lock_dir / "auto_upgrade.lck"
33
+ if not mode_file.exists():
34
+ return
35
+
36
+ try: # pragma: no cover - optional dependency failures
37
+ from django_celery_beat.models import IntervalSchedule, PeriodicTask
38
+ from django.db.utils import OperationalError, ProgrammingError
39
+ except Exception:
40
+ return
41
+
42
+ mode = mode_file.read_text().strip() or "version"
43
+ interval_minutes = 5 if mode == "latest" else 10
44
+
45
+ try:
46
+ schedule, _ = IntervalSchedule.objects.get_or_create(
47
+ every=interval_minutes, period=IntervalSchedule.MINUTES
48
+ )
49
+ PeriodicTask.objects.update_or_create(
50
+ name=AUTO_UPGRADE_TASK_NAME,
51
+ defaults={
52
+ "interval": schedule,
53
+ "task": AUTO_UPGRADE_TASK_PATH,
54
+ },
55
+ )
56
+ except (OperationalError, ProgrammingError): # pragma: no cover - DB not ready
57
+ return
core/backends.py CHANGED
@@ -1,12 +1,69 @@
1
1
  """Custom authentication backends for the core app."""
2
2
 
3
+ import contextlib
4
+ import ipaddress
5
+ import socket
6
+
7
+ from django.conf import settings
3
8
  from django.contrib.auth import get_user_model
4
9
  from django.contrib.auth.backends import ModelBackend
5
- import ipaddress
10
+ from django.core.exceptions import DisallowedHost
11
+ from django.http.request import split_domain_port
12
+ from django_otp.plugins.otp_totp.models import TOTPDevice
6
13
 
7
14
  from .models import EnergyAccount
8
15
 
9
16
 
17
+ TOTP_DEVICE_NAME = "authenticator"
18
+
19
+
20
+ class TOTPBackend(ModelBackend):
21
+ """Authenticate using a TOTP code from an enrolled authenticator app."""
22
+
23
+ def authenticate(self, request, username=None, otp_token=None, **kwargs):
24
+ if not username or otp_token in (None, ""):
25
+ return None
26
+
27
+ token = str(otp_token).strip().replace(" ", "")
28
+ if not token:
29
+ return None
30
+
31
+ UserModel = get_user_model()
32
+ try:
33
+ user = UserModel._default_manager.get_by_natural_key(username)
34
+ except UserModel.DoesNotExist:
35
+ return None
36
+
37
+ if not user.is_active:
38
+ return None
39
+
40
+ device_qs = TOTPDevice.objects.filter(user=user, confirmed=True)
41
+ if TOTP_DEVICE_NAME:
42
+ device_qs = device_qs.filter(name=TOTP_DEVICE_NAME)
43
+
44
+ device = device_qs.order_by("-id").first()
45
+ if device is None:
46
+ return None
47
+
48
+ try:
49
+ verified = device.verify_token(token)
50
+ except Exception:
51
+ return None
52
+
53
+ if not verified:
54
+ return None
55
+
56
+ user.otp_device = device
57
+ return user
58
+
59
+ def get_user(self, user_id):
60
+ UserModel = get_user_model()
61
+ try:
62
+ return UserModel._default_manager.get(pk=user_id)
63
+ except UserModel.DoesNotExist:
64
+ return None
65
+
66
+
10
67
  class RFIDBackend:
11
68
  """Authenticate using a user's RFID."""
12
69
 
@@ -32,19 +89,67 @@ class RFIDBackend:
32
89
  return None
33
90
 
34
91
 
92
+ def _collect_local_ip_addresses():
93
+ """Return IP addresses assigned to the current machine."""
94
+
95
+ hosts = {socket.gethostname().strip()}
96
+ with contextlib.suppress(Exception):
97
+ hosts.add(socket.getfqdn().strip())
98
+
99
+ addresses = set()
100
+ for host in filter(None, hosts):
101
+ with contextlib.suppress(OSError):
102
+ _, _, ip_list = socket.gethostbyname_ex(host)
103
+ for candidate in ip_list:
104
+ with contextlib.suppress(ValueError):
105
+ addresses.add(ipaddress.ip_address(candidate))
106
+ with contextlib.suppress(OSError):
107
+ for info in socket.getaddrinfo(host, None, family=socket.AF_UNSPEC):
108
+ sockaddr = info[-1]
109
+ if not sockaddr:
110
+ continue
111
+ raw_address = sockaddr[0]
112
+ if isinstance(raw_address, bytes):
113
+ with contextlib.suppress(UnicodeDecodeError):
114
+ raw_address = raw_address.decode()
115
+ if isinstance(raw_address, str):
116
+ if "%" in raw_address:
117
+ raw_address = raw_address.split("%", 1)[0]
118
+ with contextlib.suppress(ValueError):
119
+ addresses.add(ipaddress.ip_address(raw_address))
120
+ return tuple(sorted(addresses, key=str))
121
+
122
+
35
123
  class LocalhostAdminBackend(ModelBackend):
36
124
  """Allow default admin credentials only from local networks."""
37
125
 
38
- _ALLOWED_NETWORKS = [
126
+ _ALLOWED_NETWORKS = (
39
127
  ipaddress.ip_network("::1/128"),
40
128
  ipaddress.ip_network("127.0.0.0/8"),
41
- ipaddress.ip_network("192.168.0.0/16"),
42
- ipaddress.ip_network("172.16.0.0/12"),
43
129
  ipaddress.ip_network("10.42.0.0/16"),
44
- ]
130
+ ipaddress.ip_network("192.168.0.0/16"),
131
+ )
132
+ _CONTROL_ALLOWED_NETWORKS = (ipaddress.ip_network("10.0.0.0/8"),)
133
+ _LOCAL_IPS = _collect_local_ip_addresses()
134
+
135
+ def _iter_allowed_networks(self):
136
+ yield from self._ALLOWED_NETWORKS
137
+ if getattr(settings, "NODE_ROLE", "") == "Control":
138
+ yield from self._CONTROL_ALLOWED_NETWORKS
45
139
 
46
140
  def authenticate(self, request, username=None, password=None, **kwargs):
47
141
  if username == "admin" and password == "admin" and request is not None:
142
+ try:
143
+ host = request.get_host()
144
+ except DisallowedHost:
145
+ return None
146
+ host, _port = split_domain_port(host)
147
+ if host.startswith("[") and host.endswith("]"):
148
+ host = host[1:-1]
149
+ try:
150
+ ipaddress.ip_address(host)
151
+ except ValueError:
152
+ return None
48
153
  forwarded = request.META.get("HTTP_X_FORWARDED_FOR")
49
154
  if forwarded:
50
155
  remote = forwarded.split(",")[0].strip()
@@ -54,7 +159,9 @@ class LocalhostAdminBackend(ModelBackend):
54
159
  ip = ipaddress.ip_address(remote)
55
160
  except ValueError:
56
161
  return None
57
- allowed = any(ip in net for net in self._ALLOWED_NETWORKS)
162
+ allowed = any(ip in net for net in self._iter_allowed_networks())
163
+ if not allowed and ip in self._LOCAL_IPS:
164
+ allowed = True
58
165
  if not allowed:
59
166
  return None
60
167
  User = get_user_model()
@@ -65,11 +172,21 @@ class LocalhostAdminBackend(ModelBackend):
65
172
  "is_superuser": True,
66
173
  },
67
174
  )
175
+ if not created and not user.is_active:
176
+ return None
177
+ arthexis_user = (
178
+ User.all_objects.filter(username="arthexis").exclude(pk=user.pk).first()
179
+ )
68
180
  if created:
181
+ if arthexis_user and user.operate_as_id is None:
182
+ user.operate_as = arthexis_user
69
183
  user.set_password("admin")
70
184
  user.save()
71
185
  elif not user.check_password("admin"):
72
186
  return None
187
+ elif arthexis_user and user.operate_as_id is None:
188
+ user.operate_as = arthexis_user
189
+ user.save(update_fields=["operate_as"])
73
190
  return user
74
191
  return super().authenticate(request, username, password, **kwargs)
75
192
 
@@ -79,4 +196,3 @@ class LocalhostAdminBackend(ModelBackend):
79
196
  return User.all_objects.get(pk=user_id)
80
197
  except User.DoesNotExist:
81
198
  return None
82
-