django-nativemojo 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.
- django_nativemojo-0.1.10.dist-info/LICENSE +19 -0
- django_nativemojo-0.1.10.dist-info/METADATA +96 -0
- django_nativemojo-0.1.10.dist-info/NOTICE +8 -0
- django_nativemojo-0.1.10.dist-info/RECORD +194 -0
- django_nativemojo-0.1.10.dist-info/WHEEL +4 -0
- mojo/__init__.py +3 -0
- mojo/apps/account/__init__.py +1 -0
- mojo/apps/account/admin.py +91 -0
- mojo/apps/account/apps.py +16 -0
- mojo/apps/account/migrations/0001_initial.py +77 -0
- mojo/apps/account/migrations/0002_user_is_email_verified_user_is_phone_verified.py +23 -0
- mojo/apps/account/migrations/0003_group_mojo_secrets_user_mojo_secrets.py +23 -0
- mojo/apps/account/migrations/__init__.py +0 -0
- mojo/apps/account/models/__init__.py +3 -0
- mojo/apps/account/models/group.py +98 -0
- mojo/apps/account/models/member.py +95 -0
- mojo/apps/account/models/pkey.py +18 -0
- mojo/apps/account/models/user.py +211 -0
- mojo/apps/account/rest/__init__.py +3 -0
- mojo/apps/account/rest/group.py +25 -0
- mojo/apps/account/rest/user.py +47 -0
- mojo/apps/account/utils/__init__.py +0 -0
- mojo/apps/account/utils/jwtoken.py +72 -0
- mojo/apps/account/utils/passkeys.py +54 -0
- mojo/apps/fileman/README.md +549 -0
- mojo/apps/fileman/__init__.py +0 -0
- mojo/apps/fileman/apps.py +15 -0
- mojo/apps/fileman/backends/__init__.py +117 -0
- mojo/apps/fileman/backends/base.py +319 -0
- mojo/apps/fileman/backends/filesystem.py +397 -0
- mojo/apps/fileman/backends/s3.py +398 -0
- mojo/apps/fileman/examples/configurations.py +378 -0
- mojo/apps/fileman/examples/usage_example.py +665 -0
- mojo/apps/fileman/management/__init__.py +1 -0
- mojo/apps/fileman/management/commands/__init__.py +1 -0
- mojo/apps/fileman/management/commands/cleanup_expired_uploads.py +222 -0
- mojo/apps/fileman/models/__init__.py +7 -0
- mojo/apps/fileman/models/file.py +292 -0
- mojo/apps/fileman/models/manager.py +227 -0
- mojo/apps/fileman/models/render.py +0 -0
- mojo/apps/fileman/rest/__init__ +0 -0
- mojo/apps/fileman/rest/__init__.py +23 -0
- mojo/apps/fileman/rest/fileman.py +13 -0
- mojo/apps/fileman/rest/upload.py +92 -0
- mojo/apps/fileman/utils/__init__.py +19 -0
- mojo/apps/fileman/utils/upload.py +616 -0
- mojo/apps/incident/__init__.py +1 -0
- mojo/apps/incident/handlers/__init__.py +3 -0
- mojo/apps/incident/handlers/event_handlers.py +142 -0
- mojo/apps/incident/migrations/0001_initial.py +83 -0
- mojo/apps/incident/migrations/0002_rename_bundle_ruleset_bundle_minutes_event_hostname_and_more.py +44 -0
- mojo/apps/incident/migrations/0003_alter_event_model_id.py +18 -0
- mojo/apps/incident/migrations/0004_alter_incident_model_id.py +18 -0
- mojo/apps/incident/migrations/__init__.py +0 -0
- mojo/apps/incident/models/__init__.py +3 -0
- mojo/apps/incident/models/event.py +135 -0
- mojo/apps/incident/models/incident.py +33 -0
- mojo/apps/incident/models/rule.py +247 -0
- mojo/apps/incident/parsers/__init__.py +0 -0
- mojo/apps/incident/parsers/ossec/__init__.py +1 -0
- mojo/apps/incident/parsers/ossec/core.py +82 -0
- mojo/apps/incident/parsers/ossec/parsed.py +23 -0
- mojo/apps/incident/parsers/ossec/rules.py +124 -0
- mojo/apps/incident/parsers/ossec/utils.py +169 -0
- mojo/apps/incident/reporter.py +42 -0
- mojo/apps/incident/rest/__init__.py +2 -0
- mojo/apps/incident/rest/event.py +23 -0
- mojo/apps/incident/rest/ossec.py +22 -0
- mojo/apps/logit/__init__.py +0 -0
- mojo/apps/logit/admin.py +37 -0
- mojo/apps/logit/migrations/0001_initial.py +32 -0
- mojo/apps/logit/migrations/0002_log_duid_log_payload_log_username.py +28 -0
- mojo/apps/logit/migrations/0003_log_level.py +18 -0
- mojo/apps/logit/migrations/__init__.py +0 -0
- mojo/apps/logit/models/__init__.py +1 -0
- mojo/apps/logit/models/log.py +57 -0
- mojo/apps/logit/rest.py +9 -0
- mojo/apps/metrics/README.md +79 -0
- mojo/apps/metrics/__init__.py +12 -0
- mojo/apps/metrics/redis_metrics.py +331 -0
- mojo/apps/metrics/rest/__init__.py +1 -0
- mojo/apps/metrics/rest/base.py +152 -0
- mojo/apps/metrics/rest/db.py +0 -0
- mojo/apps/metrics/utils.py +227 -0
- mojo/apps/notify/README.md +91 -0
- mojo/apps/notify/README_NOTIFICATIONS.md +566 -0
- mojo/apps/notify/__init__.py +0 -0
- mojo/apps/notify/admin.py +52 -0
- mojo/apps/notify/handlers/__init__.py +0 -0
- mojo/apps/notify/handlers/example_handlers.py +516 -0
- mojo/apps/notify/handlers/ses/__init__.py +25 -0
- mojo/apps/notify/handlers/ses/bounce.py +0 -0
- mojo/apps/notify/handlers/ses/complaint.py +25 -0
- mojo/apps/notify/handlers/ses/message.py +86 -0
- mojo/apps/notify/management/__init__.py +0 -0
- mojo/apps/notify/management/commands/__init__.py +1 -0
- mojo/apps/notify/management/commands/process_notifications.py +370 -0
- mojo/apps/notify/mod +0 -0
- mojo/apps/notify/models/__init__.py +12 -0
- mojo/apps/notify/models/account.py +128 -0
- mojo/apps/notify/models/attachment.py +24 -0
- mojo/apps/notify/models/bounce.py +68 -0
- mojo/apps/notify/models/complaint.py +40 -0
- mojo/apps/notify/models/inbox.py +113 -0
- mojo/apps/notify/models/inbox_message.py +173 -0
- mojo/apps/notify/models/outbox.py +129 -0
- mojo/apps/notify/models/outbox_message.py +288 -0
- mojo/apps/notify/models/template.py +30 -0
- mojo/apps/notify/providers/__init__.py +0 -0
- mojo/apps/notify/providers/aws.py +73 -0
- mojo/apps/notify/rest/__init__.py +0 -0
- mojo/apps/notify/rest/ses.py +0 -0
- mojo/apps/notify/utils/__init__.py +2 -0
- mojo/apps/notify/utils/notifications.py +404 -0
- mojo/apps/notify/utils/parsing.py +202 -0
- mojo/apps/notify/utils/render.py +144 -0
- mojo/apps/tasks/README.md +118 -0
- mojo/apps/tasks/__init__.py +11 -0
- mojo/apps/tasks/manager.py +489 -0
- mojo/apps/tasks/rest/__init__.py +2 -0
- mojo/apps/tasks/rest/hooks.py +0 -0
- mojo/apps/tasks/rest/tasks.py +62 -0
- mojo/apps/tasks/runner.py +174 -0
- mojo/apps/tasks/tq_handlers.py +14 -0
- mojo/decorators/__init__.py +3 -0
- mojo/decorators/auth.py +25 -0
- mojo/decorators/cron.py +31 -0
- mojo/decorators/http.py +132 -0
- mojo/decorators/validate.py +14 -0
- mojo/errors.py +88 -0
- mojo/helpers/__init__.py +0 -0
- mojo/helpers/aws/__init__.py +0 -0
- mojo/helpers/aws/client.py +8 -0
- mojo/helpers/aws/s3.py +268 -0
- mojo/helpers/aws/setup_email.py +0 -0
- mojo/helpers/cron.py +79 -0
- mojo/helpers/crypto/__init__.py +4 -0
- mojo/helpers/crypto/aes.py +60 -0
- mojo/helpers/crypto/hash.py +59 -0
- mojo/helpers/crypto/privpub/__init__.py +1 -0
- mojo/helpers/crypto/privpub/hybrid.py +97 -0
- mojo/helpers/crypto/privpub/rsa.py +104 -0
- mojo/helpers/crypto/sign.py +36 -0
- mojo/helpers/crypto/too.l.py +25 -0
- mojo/helpers/crypto/utils.py +26 -0
- mojo/helpers/daemon.py +94 -0
- mojo/helpers/dates.py +69 -0
- mojo/helpers/dns/__init__.py +0 -0
- mojo/helpers/dns/godaddy.py +62 -0
- mojo/helpers/filetypes.py +128 -0
- mojo/helpers/logit.py +310 -0
- mojo/helpers/modules.py +95 -0
- mojo/helpers/paths.py +63 -0
- mojo/helpers/redis.py +10 -0
- mojo/helpers/request.py +89 -0
- mojo/helpers/request_parser.py +269 -0
- mojo/helpers/response.py +14 -0
- mojo/helpers/settings.py +146 -0
- mojo/helpers/sysinfo.py +140 -0
- mojo/helpers/ua.py +0 -0
- mojo/middleware/__init__.py +0 -0
- mojo/middleware/auth.py +26 -0
- mojo/middleware/logging.py +55 -0
- mojo/middleware/mojo.py +21 -0
- mojo/migrations/0001_initial.py +32 -0
- mojo/migrations/__init__.py +0 -0
- mojo/models/__init__.py +2 -0
- mojo/models/meta.py +262 -0
- mojo/models/rest.py +538 -0
- mojo/models/secrets.py +59 -0
- mojo/rest/__init__.py +1 -0
- mojo/rest/info.py +26 -0
- mojo/serializers/__init__.py +0 -0
- mojo/serializers/models.py +165 -0
- mojo/serializers/openapi.py +188 -0
- mojo/urls.py +38 -0
- mojo/ws4redis/README.md +174 -0
- mojo/ws4redis/__init__.py +2 -0
- mojo/ws4redis/client.py +283 -0
- mojo/ws4redis/connection.py +327 -0
- mojo/ws4redis/exceptions.py +32 -0
- mojo/ws4redis/redis.py +183 -0
- mojo/ws4redis/servers/__init__.py +0 -0
- mojo/ws4redis/servers/base.py +86 -0
- mojo/ws4redis/servers/django.py +171 -0
- mojo/ws4redis/servers/uwsgi.py +63 -0
- mojo/ws4redis/settings.py +45 -0
- mojo/ws4redis/utf8validator.py +128 -0
- mojo/ws4redis/websocket.py +403 -0
- testit/__init__.py +0 -0
- testit/client.py +147 -0
- testit/faker.py +20 -0
- testit/helpers.py +198 -0
- testit/runner.py +262 -0
@@ -0,0 +1,269 @@
|
|
1
|
+
import ujson
|
2
|
+
from typing import Any, Dict, List
|
3
|
+
from django.http import QueryDict
|
4
|
+
from objict import objict, nobjict
|
5
|
+
|
6
|
+
|
7
|
+
class RequestDataParser:
|
8
|
+
"""
|
9
|
+
A robust parser for Django request data that consolidates GET, POST, JSON, and FILES
|
10
|
+
into a single objict with support for dotted notation and array handling.
|
11
|
+
"""
|
12
|
+
|
13
|
+
def __init__(self, use_objict=True):
|
14
|
+
"""
|
15
|
+
Initialize parser.
|
16
|
+
|
17
|
+
Args:
|
18
|
+
use_objict: Whether to use objict() or regular dict() for nested structures
|
19
|
+
"""
|
20
|
+
self.dict_factory = objict if use_objict else dict
|
21
|
+
|
22
|
+
def parse(self, request) -> 'objict':
|
23
|
+
"""
|
24
|
+
Main entry point - parses all request data into a single objict.
|
25
|
+
|
26
|
+
Args:
|
27
|
+
request: Django HttpRequest object
|
28
|
+
|
29
|
+
Returns:
|
30
|
+
objict containing all parsed request data
|
31
|
+
"""
|
32
|
+
result = self.dict_factory()
|
33
|
+
|
34
|
+
# Process in order of precedence (later sources can override earlier ones)
|
35
|
+
self._process_query_params(request.GET, result)
|
36
|
+
self._process_form_data(request, result)
|
37
|
+
self._process_json_data(request, result)
|
38
|
+
self._process_files(request.FILES, result)
|
39
|
+
|
40
|
+
return result
|
41
|
+
|
42
|
+
def _process_query_params(self, query_dict: QueryDict, target: 'objict') -> None:
|
43
|
+
"""Process GET parameters from QueryDict."""
|
44
|
+
if not query_dict:
|
45
|
+
return
|
46
|
+
|
47
|
+
for key in query_dict.keys():
|
48
|
+
values = query_dict.getlist(key)
|
49
|
+
normalized_key = self._normalize_key(key)
|
50
|
+
self._set_nested_value(target, normalized_key, values)
|
51
|
+
|
52
|
+
def _process_form_data(self, request, target: 'objict') -> None:
|
53
|
+
"""Process POST form data (non-JSON requests)."""
|
54
|
+
if request.method not in {'POST', 'PUT', 'PATCH', 'DELETE'}:
|
55
|
+
return
|
56
|
+
|
57
|
+
# Skip if this is a JSON request
|
58
|
+
content_type = getattr(request, 'content_type', '').lower()
|
59
|
+
if 'application/json' in content_type:
|
60
|
+
return
|
61
|
+
|
62
|
+
if not request.POST:
|
63
|
+
return
|
64
|
+
|
65
|
+
for key in request.POST.keys():
|
66
|
+
values = request.POST.getlist(key)
|
67
|
+
normalized_key = self._normalize_key(key)
|
68
|
+
self._set_nested_value(target, normalized_key, values)
|
69
|
+
|
70
|
+
def _process_json_data(self, request, target: 'objict') -> None:
|
71
|
+
"""Process JSON request body."""
|
72
|
+
if request.method not in {'POST', 'PUT', 'PATCH', 'DELETE'}:
|
73
|
+
return
|
74
|
+
|
75
|
+
content_type = getattr(request, 'content_type', '').lower()
|
76
|
+
if 'application/json' not in content_type:
|
77
|
+
return
|
78
|
+
|
79
|
+
try:
|
80
|
+
if hasattr(request, 'body') and request.body:
|
81
|
+
json_data = ujson.loads(request.body.decode('utf-8'))
|
82
|
+
if isinstance(json_data, dict):
|
83
|
+
self._merge_dict_data(json_data, target)
|
84
|
+
else:
|
85
|
+
# Handle case where JSON root is not an object
|
86
|
+
target['_json_data'] = json_data
|
87
|
+
except Exception as e:
|
88
|
+
# Store error info for debugging
|
89
|
+
target['_json_parse_error'] = str(e)
|
90
|
+
|
91
|
+
def _process_files(self, files_dict, target: 'objict') -> None:
|
92
|
+
"""Process uploaded files."""
|
93
|
+
if not files_dict:
|
94
|
+
return
|
95
|
+
|
96
|
+
files_obj = nobjict()
|
97
|
+
|
98
|
+
for key in files_dict.keys():
|
99
|
+
files = files_dict.getlist(key)
|
100
|
+
normalized_key = self._normalize_key(key)
|
101
|
+
|
102
|
+
# Store single file directly, multiple files as list
|
103
|
+
file_value = files[0] if len(files) == 1 else files
|
104
|
+
self._set_nested_value(files_obj, normalized_key, [file_value])
|
105
|
+
|
106
|
+
if files_obj:
|
107
|
+
target['files'] = files_obj
|
108
|
+
|
109
|
+
def _normalize_key(self, key: str) -> str:
|
110
|
+
"""
|
111
|
+
Normalize keys by removing array notation brackets.
|
112
|
+
|
113
|
+
Examples:
|
114
|
+
'tags[]' -> 'tags'
|
115
|
+
'user[name][]' -> 'user.name'
|
116
|
+
'items[0][name]' -> 'items.0.name'
|
117
|
+
"""
|
118
|
+
# Remove trailing []
|
119
|
+
key = key.rstrip('[]')
|
120
|
+
|
121
|
+
# Convert bracket notation to dot notation
|
122
|
+
# user[name] -> user.name
|
123
|
+
# items[0][name] -> items.0.name
|
124
|
+
import re
|
125
|
+
key = re.sub(r'\[([^\]]+)\]', r'.\1', key)
|
126
|
+
|
127
|
+
return key
|
128
|
+
|
129
|
+
def _merge_dict_data(self, source: Dict[str, Any], target: 'objict') -> None:
|
130
|
+
"""Recursively merge dictionary data into target objict."""
|
131
|
+
for key, value in source.items():
|
132
|
+
self._set_nested_value(target, key, [value])
|
133
|
+
|
134
|
+
def _set_nested_value(self, target: 'objict', dotted_key: str, values: List[Any]) -> None:
|
135
|
+
"""
|
136
|
+
Set a value in nested objict structure using dotted key notation.
|
137
|
+
|
138
|
+
Args:
|
139
|
+
target: Target objict to modify
|
140
|
+
dotted_key: Key like 'user.profile.name' or 'tags'
|
141
|
+
values: List of values (handles single values and arrays)
|
142
|
+
"""
|
143
|
+
if not dotted_key:
|
144
|
+
return
|
145
|
+
|
146
|
+
parts = dotted_key.split('.')
|
147
|
+
current = target
|
148
|
+
|
149
|
+
# Navigate to the parent of the final key
|
150
|
+
for part in parts[:-1]:
|
151
|
+
if not part: # Skip empty parts from double dots
|
152
|
+
continue
|
153
|
+
|
154
|
+
if part not in current:
|
155
|
+
current[part] = self.dict_factory()
|
156
|
+
elif not isinstance(current[part], (dict, objict, nobjict)):
|
157
|
+
# Convert existing non-dict value to dict to avoid conflicts
|
158
|
+
current[part] = self.dict_factory()
|
159
|
+
|
160
|
+
current = current[part]
|
161
|
+
|
162
|
+
final_key = parts[-1]
|
163
|
+
if not final_key: # Handle edge case of trailing dot
|
164
|
+
return
|
165
|
+
|
166
|
+
# Determine final value based on number of values
|
167
|
+
if len(values) == 0:
|
168
|
+
final_value = None
|
169
|
+
elif len(values) == 1:
|
170
|
+
final_value = values[0]
|
171
|
+
else:
|
172
|
+
final_value = values
|
173
|
+
|
174
|
+
# Handle existing values - merge into arrays if needed
|
175
|
+
if final_key in current:
|
176
|
+
existing = current[final_key]
|
177
|
+
if isinstance(existing, list):
|
178
|
+
if isinstance(final_value, list):
|
179
|
+
existing.extend(final_value)
|
180
|
+
else:
|
181
|
+
existing.append(final_value)
|
182
|
+
else:
|
183
|
+
if isinstance(final_value, list):
|
184
|
+
current[final_key] = [existing] + final_value
|
185
|
+
else:
|
186
|
+
current[final_key] = [existing, final_value]
|
187
|
+
else:
|
188
|
+
current[final_key] = final_value
|
189
|
+
|
190
|
+
|
191
|
+
# Convenience functions for backward compatibility
|
192
|
+
def parse_request_data(request) -> 'objict':
|
193
|
+
"""
|
194
|
+
Parse Django request data into a single objict.
|
195
|
+
|
196
|
+
Args:
|
197
|
+
request: Django HttpRequest object
|
198
|
+
|
199
|
+
Returns:
|
200
|
+
objict containing all parsed request data
|
201
|
+
"""
|
202
|
+
parser = RequestDataParser()
|
203
|
+
return parser.parse(request)
|
204
|
+
|
205
|
+
|
206
|
+
def parse_request_data_debug(request) -> 'objict':
|
207
|
+
"""
|
208
|
+
Parse request data with debug information printed.
|
209
|
+
"""
|
210
|
+
print("=== REQUEST PARSING DEBUG ===")
|
211
|
+
print(f"Method: {request.method}")
|
212
|
+
print(f"Content-Type: {getattr(request, 'content_type', 'Not set')}")
|
213
|
+
print(f"GET keys: {list(request.GET.keys())}")
|
214
|
+
print(f"POST keys: {list(request.POST.keys())}")
|
215
|
+
print(f"FILES keys: {list(request.FILES.keys())}")
|
216
|
+
|
217
|
+
if hasattr(request, 'body'):
|
218
|
+
body_preview = request.body[:200] if request.body else b''
|
219
|
+
print(f"Body preview: {body_preview}")
|
220
|
+
|
221
|
+
result = parse_request_data(request)
|
222
|
+
print(f"Parsed result keys: {list(result.keys())}")
|
223
|
+
print("=== END DEBUG ===")
|
224
|
+
|
225
|
+
return result
|
226
|
+
|
227
|
+
|
228
|
+
# Example usage and test cases
|
229
|
+
def test_parser():
|
230
|
+
"""
|
231
|
+
Test function to demonstrate parser capabilities.
|
232
|
+
"""
|
233
|
+
from django.http import QueryDict
|
234
|
+
from io import StringIO
|
235
|
+
import sys
|
236
|
+
|
237
|
+
# Capture output for testing
|
238
|
+
old_stdout = sys.stdout
|
239
|
+
sys.stdout = buffer = StringIO()
|
240
|
+
|
241
|
+
try:
|
242
|
+
# Mock request object for testing
|
243
|
+
class MockRequest:
|
244
|
+
def __init__(self):
|
245
|
+
self.method = 'POST'
|
246
|
+
self.content_type = 'application/json'
|
247
|
+
self.GET = QueryDict('user.name=John&tags[]=python&tags[]=django&user.age=30')
|
248
|
+
self.POST = QueryDict()
|
249
|
+
self.FILES = QueryDict()
|
250
|
+
self.body = b'{"api_key": "secret", "nested": {"value": 42}}'
|
251
|
+
|
252
|
+
request = MockRequest()
|
253
|
+
result = parse_request_data(request)
|
254
|
+
|
255
|
+
print("Test Results:")
|
256
|
+
print(f"User name: {result.get('user', {}).get('name')}")
|
257
|
+
print(f"User age: {result.get('user', {}).get('age')}")
|
258
|
+
print(f"Tags: {result.get('tags')}")
|
259
|
+
print(f"API Key: {result.get('api_key')}")
|
260
|
+
print(f"Nested value: {result.get('nested', {}).get('value')}")
|
261
|
+
|
262
|
+
finally:
|
263
|
+
sys.stdout = old_stdout
|
264
|
+
output = buffer.getvalue()
|
265
|
+
print(output)
|
266
|
+
|
267
|
+
|
268
|
+
if __name__ == "__main__":
|
269
|
+
test_parser()
|
mojo/helpers/response.py
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
import ujson
|
2
|
+
from django.http import HttpResponse
|
3
|
+
|
4
|
+
|
5
|
+
class JsonResponse(HttpResponse):
|
6
|
+
def __init__(self, data, status=200, safe=True, **kwargs):
|
7
|
+
if safe and not isinstance(data, dict):
|
8
|
+
raise TypeError(
|
9
|
+
'In order to allow non-dict objects to be serialized set the '
|
10
|
+
'safe parameter to False.'
|
11
|
+
)
|
12
|
+
kwargs.setdefault('content_type', 'application/json')
|
13
|
+
data = ujson.dumps(data)
|
14
|
+
super().__init__(content=data, status=status, **kwargs)
|
mojo/helpers/settings.py
ADDED
@@ -0,0 +1,146 @@
|
|
1
|
+
import importlib
|
2
|
+
from typing import Any, Union
|
3
|
+
|
4
|
+
UNKNOWN = Ellipsis
|
5
|
+
|
6
|
+
|
7
|
+
def load_settings_profile(context):
|
8
|
+
from mojo.helpers import modules, paths
|
9
|
+
# Set default profile
|
10
|
+
profile = "local"
|
11
|
+
# Check if a profile file exists and override profile
|
12
|
+
profile_file = paths.VAR_ROOT / "profile"
|
13
|
+
if profile_file.exists():
|
14
|
+
with open(profile_file, 'r') as file:
|
15
|
+
profile = file.read().strip()
|
16
|
+
modules.load_module_to_globals("settings.defaults", context)
|
17
|
+
modules.load_module_to_globals(f"settings.{profile}", context)
|
18
|
+
|
19
|
+
def load_settings_config(context):
|
20
|
+
# Load config from django.conf file
|
21
|
+
from mojo.helpers import paths
|
22
|
+
config_path = paths.VAR_ROOT / "django.conf"
|
23
|
+
if not config_path.exists():
|
24
|
+
raise Exception(f"Required configuration file not found: {config_path}")
|
25
|
+
|
26
|
+
with open(config_path, 'r') as file:
|
27
|
+
for line in file:
|
28
|
+
if '=' in line:
|
29
|
+
key, value = line.strip().split('=', 1)
|
30
|
+
value = value.strip()
|
31
|
+
if value.startswith('"') and value.endswith('"'):
|
32
|
+
value = value[1:-1]
|
33
|
+
elif value.startswith("'") and value.endswith("'"):
|
34
|
+
value = value[1:-1]
|
35
|
+
if value.startswith('f"') or value.startswith("f'"):
|
36
|
+
value = eval(value)
|
37
|
+
elif value.lower() == 'true':
|
38
|
+
value = True
|
39
|
+
elif value.lower() == 'false':
|
40
|
+
value = False
|
41
|
+
else:
|
42
|
+
try:
|
43
|
+
if '.' in value:
|
44
|
+
value = float(value)
|
45
|
+
else:
|
46
|
+
value = int(value)
|
47
|
+
except ValueError:
|
48
|
+
pass
|
49
|
+
context[key.strip()] = value
|
50
|
+
|
51
|
+
if context.get("ALLOW_ADMIN_SITE", True):
|
52
|
+
if "django.contrib.admin" not in context["INSTALLED_APPS"]:
|
53
|
+
context["INSTALLED_APPS"].insert(0, "django.contrib.admin")
|
54
|
+
|
55
|
+
|
56
|
+
class SettingsHelper:
|
57
|
+
"""
|
58
|
+
A helper class for accessing Django settings with support for:
|
59
|
+
- Default values if settings are missing.
|
60
|
+
- App-specific settings loading from `{app_name}.settings`.
|
61
|
+
- Dictionary-style (`settings["KEY"]`) and attribute-style (`settings.KEY`) access.
|
62
|
+
"""
|
63
|
+
|
64
|
+
def __init__(self, root_settings=None, defaults=None):
|
65
|
+
"""
|
66
|
+
Initialize the settings helper.
|
67
|
+
|
68
|
+
:param root_settings: The primary settings source (Django settings or a dictionary).
|
69
|
+
:param defaults: An optional defaults module or dictionary.
|
70
|
+
"""
|
71
|
+
self.root = root_settings
|
72
|
+
self.defaults = defaults
|
73
|
+
self._app_cache = {}
|
74
|
+
|
75
|
+
def load_settings(self):
|
76
|
+
from django.conf import settings as django_settings
|
77
|
+
self.root = django_settings
|
78
|
+
|
79
|
+
def get_app_settings(self, app_name: str) -> "SettingsHelper":
|
80
|
+
"""
|
81
|
+
Get settings for a specific app, attempting to load `{app_name}.settings`.
|
82
|
+
|
83
|
+
:param app_name: The Django app name.
|
84
|
+
:return: A `SettingsHelper` instance for the app settings.
|
85
|
+
"""
|
86
|
+
key = f"{app_name.upper()}_SETTINGS"
|
87
|
+
|
88
|
+
if key in self._app_cache:
|
89
|
+
return self._app_cache[key]
|
90
|
+
|
91
|
+
try:
|
92
|
+
app_defaults = importlib.import_module(f"{app_name}.settings")
|
93
|
+
except ModuleNotFoundError:
|
94
|
+
app_defaults = {}
|
95
|
+
|
96
|
+
self._app_cache[key] = SettingsHelper(self.get(key, {}), app_defaults)
|
97
|
+
return self._app_cache[key]
|
98
|
+
|
99
|
+
def get(self, name: str, default: Any = UNKNOWN) -> Any:
|
100
|
+
"""
|
101
|
+
Retrieve a setting, falling back to defaults if needed.
|
102
|
+
|
103
|
+
:param name: The setting name.
|
104
|
+
:param default: The default value if the setting is not found.
|
105
|
+
:return: The setting value or the provided default.
|
106
|
+
"""
|
107
|
+
if self.root is None:
|
108
|
+
self.load_settings()
|
109
|
+
if isinstance(self.root, dict):
|
110
|
+
value = self.root.get(name, UNKNOWN)
|
111
|
+
else:
|
112
|
+
value = getattr(self.root, name, UNKNOWN)
|
113
|
+
|
114
|
+
return value if value is not UNKNOWN else self.get_default(name, default)
|
115
|
+
|
116
|
+
def get_default(self, name: str, default: Any = None) -> Any:
|
117
|
+
"""
|
118
|
+
Retrieve a setting from the defaults, if provided.
|
119
|
+
|
120
|
+
:param name: The setting name.
|
121
|
+
:param default: The default value if the setting is not found in defaults.
|
122
|
+
:return: The default setting value.
|
123
|
+
"""
|
124
|
+
if isinstance(self.defaults, dict):
|
125
|
+
return self.defaults.get(name, default)
|
126
|
+
|
127
|
+
return getattr(self.defaults, name, default)
|
128
|
+
|
129
|
+
def is_app_installed(self, app_label):
|
130
|
+
return app_label in self.get("INSTALLED_APPS")
|
131
|
+
|
132
|
+
def __getattr__(self, name: str) -> Any:
|
133
|
+
"""
|
134
|
+
Access settings as attributes (`settings.KEY`).
|
135
|
+
"""
|
136
|
+
return self.get(name, None)
|
137
|
+
|
138
|
+
def __getitem__(self, key: str) -> Any:
|
139
|
+
"""
|
140
|
+
Access settings as dictionary keys (`settings["KEY"]`).
|
141
|
+
"""
|
142
|
+
return self.get(key)
|
143
|
+
|
144
|
+
|
145
|
+
# Create a global settings helper for accessing Django settings
|
146
|
+
settings = SettingsHelper()
|
mojo/helpers/sysinfo.py
ADDED
@@ -0,0 +1,140 @@
|
|
1
|
+
import platform
|
2
|
+
import socket
|
3
|
+
|
4
|
+
from datetime import datetime
|
5
|
+
import time
|
6
|
+
|
7
|
+
try:
|
8
|
+
import psutil
|
9
|
+
except ImportError:
|
10
|
+
psutil = None
|
11
|
+
|
12
|
+
|
13
|
+
def get_host_ip():
|
14
|
+
"""Get the IP address associated with the current hostname."""
|
15
|
+
hostname = socket.gethostname()
|
16
|
+
return socket.gethostbyname(hostname)
|
17
|
+
|
18
|
+
def get_tcp_established_count():
|
19
|
+
"""Return the count of established TCP connections."""
|
20
|
+
return len(get_tcp_established())
|
21
|
+
|
22
|
+
def get_tcp_established(filter_type=None):
|
23
|
+
"""
|
24
|
+
Get a list of established TCP connections, optionally filtering by type.
|
25
|
+
|
26
|
+
:param filter_type: Optionally filter by type ('https', 'redis', 'postgres', etc.)
|
27
|
+
:return: List of connections according to filter
|
28
|
+
"""
|
29
|
+
connections = psutil.net_connections(kind="tcp")
|
30
|
+
established_connections = [c for c in connections if c.status == psutil.CONN_ESTABLISHED]
|
31
|
+
return filter_connections(established_connections, filter_type)
|
32
|
+
|
33
|
+
def filter_connections(connections, filter_type):
|
34
|
+
"""
|
35
|
+
Filter connections based on the specified filter type.
|
36
|
+
|
37
|
+
:param connections: List of connections
|
38
|
+
:param filter_type: Type to filter (options: 'https', 'redis', 'postgres')
|
39
|
+
:return: List of filtered connections
|
40
|
+
"""
|
41
|
+
filters = {
|
42
|
+
"https": lambda c: c.laddr.port == 443,
|
43
|
+
"redis": lambda c: c.raddr.port == 6379,
|
44
|
+
"postgres": lambda c: c.raddr.port == 5432,
|
45
|
+
"unknown": lambda c: c.raddr.port not in [5432, 6379] and c.laddr.port != 443
|
46
|
+
}
|
47
|
+
|
48
|
+
if filter_type in filters:
|
49
|
+
return [c for c in connections if filters[filter_type](c)]
|
50
|
+
elif filter_type and ":" in filter_type:
|
51
|
+
addr, port = filter_type.split(':')
|
52
|
+
port = int(port)
|
53
|
+
return [c for c in connections if (addr == "raddr" and c.raddr.port == port) or (addr == "laddr" and c.laddr.port == port)]
|
54
|
+
|
55
|
+
return connections
|
56
|
+
|
57
|
+
def connections_to_dict(connections):
|
58
|
+
"""
|
59
|
+
Convert a list of connections to a dictionary representation.
|
60
|
+
|
61
|
+
:param connections: List of connections
|
62
|
+
:return: List of dictionaries representing connections
|
63
|
+
"""
|
64
|
+
return [
|
65
|
+
{
|
66
|
+
"id": idx,
|
67
|
+
"type": c.type.name,
|
68
|
+
"status": c.status,
|
69
|
+
"family": c.family.name,
|
70
|
+
"raddr": {
|
71
|
+
"port": c.raddr.port if c.raddr else None,
|
72
|
+
"ip": c.raddr.ip if c.raddr else None,
|
73
|
+
},
|
74
|
+
"laddr": {
|
75
|
+
"port": c.laddr.port,
|
76
|
+
"ip": c.laddr.ip
|
77
|
+
}
|
78
|
+
}
|
79
|
+
for idx, c in enumerate(connections, start=1)
|
80
|
+
]
|
81
|
+
|
82
|
+
|
83
|
+
def get_host_info(include_versions=False, include_blocked=False):
|
84
|
+
"""
|
85
|
+
Gather information about the host.
|
86
|
+
|
87
|
+
:param include_versions: Include software versions if True
|
88
|
+
:param include_blocked: Include blocked hosts if True
|
89
|
+
:return: Dictionary with host information
|
90
|
+
"""
|
91
|
+
mem = psutil.virtual_memory()
|
92
|
+
disk = psutil.disk_usage('/')
|
93
|
+
net = psutil.net_io_counters()
|
94
|
+
|
95
|
+
host_info = {
|
96
|
+
"time": time.time(),
|
97
|
+
"datetime": datetime.now().isoformat(),
|
98
|
+
"version": version.VERSION, # check if this needs to be imported correctly or version info retrieval needs adaptation
|
99
|
+
"os": {
|
100
|
+
"system": platform.system(),
|
101
|
+
"version": platform.version(),
|
102
|
+
"hostname": platform.node(),
|
103
|
+
"release": platform.release(),
|
104
|
+
"processor": platform.processor(),
|
105
|
+
"machine": platform.machine()
|
106
|
+
},
|
107
|
+
"boot_time": psutil.boot_time(),
|
108
|
+
"cpu_load": psutil.cpu_percent(),
|
109
|
+
"cpus_load": psutil.cpu_percent(percpu=True),
|
110
|
+
"memory": {
|
111
|
+
"total": mem.total,
|
112
|
+
"used": mem.used,
|
113
|
+
"available": mem.available,
|
114
|
+
"percent": mem.percent
|
115
|
+
},
|
116
|
+
"disk": {
|
117
|
+
"total": disk.total,
|
118
|
+
"used": disk.used,
|
119
|
+
"free": disk.free,
|
120
|
+
"percent": disk.percent
|
121
|
+
},
|
122
|
+
"users": psutil.users(),
|
123
|
+
"cpu": {
|
124
|
+
"count": psutil.cpu_count(),
|
125
|
+
"freq": psutil.cpu_freq()._asdict() if psutil.cpu_freq() else None,
|
126
|
+
},
|
127
|
+
"network": {
|
128
|
+
"tcp_cons": get_tcp_established_count(),
|
129
|
+
"bytes_sent": net.bytes_sent,
|
130
|
+
"bytes_recv": net.bytes_recv,
|
131
|
+
"packets_sent": net.packets_sent,
|
132
|
+
"packets_recv": net.packets_recv,
|
133
|
+
"errin": net.errin,
|
134
|
+
"errout": net.errout,
|
135
|
+
"dropin": net.dropin,
|
136
|
+
"dropout": net.dropout
|
137
|
+
}
|
138
|
+
}
|
139
|
+
|
140
|
+
return host_info
|
mojo/helpers/ua.py
ADDED
File without changes
|
File without changes
|
mojo/middleware/auth.py
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
from django.utils.deprecation import MiddlewareMixin
|
2
|
+
# from django.http import JsonResponse
|
3
|
+
from mojo.helpers.response import JsonResponse
|
4
|
+
from mojo.apps.account.utils.jwtoken import JWToken
|
5
|
+
from mojo.apps.account.models.user import User
|
6
|
+
from mojo.helpers import request as rhelper
|
7
|
+
from mojo.helpers.request import get_user_agent
|
8
|
+
from objict import objict
|
9
|
+
|
10
|
+
|
11
|
+
|
12
|
+
class AuthenticationMiddleware(MiddlewareMixin):
|
13
|
+
def process_request(self, request):
|
14
|
+
token = request.META.get('HTTP_AUTHORIZATION', None)
|
15
|
+
if token is None:
|
16
|
+
return
|
17
|
+
prefix, token = token.split()
|
18
|
+
request.auth_token = objict(prefix=prefix, token=token)
|
19
|
+
if prefix.lower() != 'bearer':
|
20
|
+
return
|
21
|
+
# decode data to find the user
|
22
|
+
user, error = User.validate_jwt(token)
|
23
|
+
if error is not None:
|
24
|
+
return JsonResponse({'error': error}, status=401)
|
25
|
+
request.user = user
|
26
|
+
user.touch()
|
@@ -0,0 +1,55 @@
|
|
1
|
+
from mojo.apps.logit.models import Log
|
2
|
+
from mojo.helpers.settings import settings
|
3
|
+
from mojo.helpers import logit
|
4
|
+
from mojo.helpers.response import JsonResponse
|
5
|
+
|
6
|
+
API_PREFIX = "/".join([settings.get("MOJO_PREFIX", "api/").rstrip("/"), ""])
|
7
|
+
LOGIT_DB_ALL = settings.get("LOGIT_DB_ALL", False)
|
8
|
+
LOGIT_FILE_ALL = settings.get("LOGIT_FILE_ALL", False)
|
9
|
+
LOGIT_RETURN_REAL_ERROR = settings.get("LOGIT_RETURN_REAL_ERROR", True)
|
10
|
+
LOGGER = logit.get_logger("requests", "requests.log")
|
11
|
+
ERROR_LOGGER = logit.get_logger("error", "error.log")
|
12
|
+
LOGIT_NO_LOG_PREFIX = settings.get("LOGIT_NO_LOG_PREFIX", [])
|
13
|
+
|
14
|
+
class LoggerMiddleware:
|
15
|
+
def __init__(self, get_response):
|
16
|
+
self.get_response = get_response
|
17
|
+
|
18
|
+
def __call__(self, request):
|
19
|
+
# Log the request before calling get_response
|
20
|
+
self.log_request(request)
|
21
|
+
try:
|
22
|
+
response = self.get_response(request)
|
23
|
+
except Exception as e:
|
24
|
+
# Log or store the exception here
|
25
|
+
err = ERROR_LOGGER.exception()
|
26
|
+
Log.logit(request, err, "api_error")
|
27
|
+
error = "system error"
|
28
|
+
if LOGIT_RETURN_REAL_ERROR:
|
29
|
+
error = str(e)
|
30
|
+
response = JsonResponse(dict(status=False, error=error), status=500)
|
31
|
+
# Log the response after get_response has been called
|
32
|
+
self.log_response(request, response)
|
33
|
+
return response
|
34
|
+
|
35
|
+
def can_log(self, request):
|
36
|
+
prefixes = LOGIT_NO_LOG_PREFIX
|
37
|
+
if not isinstance(prefixes, (list, set, tuple)) or not prefixes:
|
38
|
+
return True
|
39
|
+
return not any(request.path.startswith(prefix) for prefix in prefixes)
|
40
|
+
|
41
|
+
def log_request(self, request):
|
42
|
+
if not self.can_log(request):
|
43
|
+
return
|
44
|
+
if LOGIT_DB_ALL:
|
45
|
+
request.request_log = Log.logit(request, request.DATA.to_json(as_string=True), "request")
|
46
|
+
if LOGIT_FILE_ALL:
|
47
|
+
LOGGER.info(f"REQUEST - {request.method} - {request.ip} - {request.path}", request.body)
|
48
|
+
|
49
|
+
def log_response(self, request, response):
|
50
|
+
if not self.can_log(request):
|
51
|
+
return
|
52
|
+
if LOGIT_DB_ALL:
|
53
|
+
Log.logit(request, response.content, "response")
|
54
|
+
if LOGIT_FILE_ALL:
|
55
|
+
LOGGER.info(f"RESPONSE - {request.method} - {request.ip} - {request.path}", response.content)
|
mojo/middleware/mojo.py
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
from mojo.helpers import request as rhelper
|
2
|
+
import time
|
3
|
+
from objict import objict
|
4
|
+
|
5
|
+
ANONYMOUS_USER = objict(is_authenticated=False)
|
6
|
+
|
7
|
+
|
8
|
+
class MojoMiddleware:
|
9
|
+
def __init__(self, get_response):
|
10
|
+
self.get_response = get_response
|
11
|
+
|
12
|
+
def __call__(self, request):
|
13
|
+
request.started_at = time.time()
|
14
|
+
request.user = ANONYMOUS_USER
|
15
|
+
request.group = None
|
16
|
+
request.request_log = None
|
17
|
+
request.ip = rhelper.get_remote_ip(request)
|
18
|
+
request.user_agent = rhelper.get_user_agent(request)
|
19
|
+
request.duid = rhelper.get_device_id(request)
|
20
|
+
request.DATA = rhelper.parse_request_data(request)
|
21
|
+
return self.get_response(request)
|