Skip to content

Commit 222a338

Browse files
authored
Merge branch 'main' into main
2 parents b4e2f74 + f281a02 commit 222a338

265 files changed

Lines changed: 3837 additions & 5394 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

batch_optimizer.py

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Batch image optimization script
4+
Does everything automatically - converts images, updates templates, creates backups
5+
6+
Usage:
7+
python batch_optimizer.py
8+
python batch_optimizer.py --no-backup
9+
"""
10+
11+
import os
12+
import sys
13+
import argparse
14+
import subprocess
15+
from pathlib import Path
16+
17+
class BatchOptimizer:
18+
def __init__(self, project_root):
19+
self.project_root = Path(project_root).resolve()
20+
# If invoked from inside website dir, avoid duplicating path
21+
if (self.project_root / "website").exists():
22+
self.website_dir = (self.project_root / "website").resolve()
23+
else:
24+
self.website_dir = self.project_root
25+
self.static_dir = self.website_dir / "custom_static" / "assets" / "img"
26+
self.templates_dir = self.website_dir / "home" / "templates"
27+
28+
def run_command(self, command, description):
29+
"""Runs a command and handles any errors"""
30+
print(f"{description}")
31+
print(f" Command: {' '.join(command)}")
32+
print("-" * 50)
33+
34+
try:
35+
result = subprocess.run(command, cwd=self.website_dir, check=True,
36+
capture_output=True, text=True)
37+
print(result.stdout)
38+
if result.stderr:
39+
print("Warnings/Info:", result.stderr)
40+
return True
41+
except subprocess.CalledProcessError as e:
42+
print(f"Error: {e}")
43+
print(f" stdout: {e.stdout}")
44+
print(f" stderr: {e.stderr}")
45+
return False
46+
47+
def install_dependencies(self):
48+
"""Installs the Python packages we need"""
49+
print("Installing dependencies...")
50+
return self.run_command([
51+
sys.executable, "-m", "pip", "install", "-r", "requirements.txt"
52+
], "Installing all project dependencies")
53+
54+
def backup_original_files(self):
55+
"""Makes a Git backup of the original files"""
56+
print("Creating Git backup...")
57+
58+
# Check if we're in a git repository
59+
if not (self.project_root / ".git").exists():
60+
print("Not in a Git repository. Skipping backup.")
61+
return True
62+
63+
# Create backup branch
64+
commands = [
65+
["git", "checkout", "-b", "image-optimization-backup"],
66+
["git", "add", "custom_static/assets/img/"],
67+
["git", "commit", "-m", "Backup: Original images before optimization"],
68+
["git", "checkout", "main"]
69+
]
70+
71+
for cmd in commands:
72+
if not self.run_command(cmd, f"Running: {' '.join(cmd)}"):
73+
return False
74+
75+
return True
76+
77+
def optimize_all_images(self):
78+
"""Converts all JPG/PNG images to WebP format"""
79+
print("Optimizing all images...")
80+
81+
if not self.static_dir.exists():
82+
print(f"Static directory not found: {self.static_dir}")
83+
return False
84+
85+
return self.run_command([
86+
sys.executable, "image_optimizer.py",
87+
"--convert-webp",
88+
"--input-dir", str(self.static_dir),
89+
"--quality-photos", "80",
90+
"--quality-graphics", "90"
91+
], "Converting all images to WebP")
92+
93+
def minify_all_svg(self):
94+
"""Makes all SVG files smaller"""
95+
print("Minifying SVG files...")
96+
97+
return self.run_command([
98+
sys.executable, "image_optimizer.py",
99+
"--minify-svg",
100+
"--input-dir", str(self.static_dir)
101+
], "Minifying all SVG files")
102+
103+
def update_templates(self):
104+
"""Updates Django templates to use the new WebP images"""
105+
print("Updating template references...")
106+
107+
return self.run_command([
108+
sys.executable, "template_updater.py",
109+
"--templates-dir", str(self.templates_dir),
110+
"--static-dir", str(self.static_dir)
111+
], "Updating Django template references")
112+
113+
def run_optimization(self, skip_backup=False):
114+
"""Runs the complete image optimization process"""
115+
print("Running complete image optimization...")
116+
print("=" * 60)
117+
118+
steps = [
119+
("Installing dependencies", self.install_dependencies),
120+
("Creating backup", self.backup_original_files) if not skip_backup else ("Skipping backup", lambda: True),
121+
("Converting images to WebP", self.optimize_all_images),
122+
("Minifying SVG files", self.minify_all_svg),
123+
("Updating templates", self.update_templates)
124+
]
125+
126+
for step_name, step_func in steps:
127+
print(f"\nStep: {step_name}")
128+
if not step_func():
129+
print(f"Failed at step: {step_name}")
130+
return False
131+
print(f"Completed: {step_name}")
132+
133+
print("\nImage optimization completed successfully!")
134+
return True
135+
136+
def main():
137+
parser = argparse.ArgumentParser(description='Batch image optimization tool')
138+
parser.add_argument('--no-backup', action='store_true', help='Skip creating backup')
139+
parser.add_argument('--project-root', default='.', help='Project root directory')
140+
141+
args = parser.parse_args()
142+
143+
# Create optimizer instance
144+
optimizer = BatchOptimizer(args.project_root)
145+
146+
# Run optimization
147+
success = optimizer.run_optimization(skip_backup=args.no_backup)
148+
149+
return 0 if success else 1
150+
151+
if __name__ == '__main__':
152+
sys.exit(main())

core/middleware.py

Lines changed: 42 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -50,23 +50,48 @@ def __call__(self, request):
5050
current_ip = self.get_client_ip(request)
5151
current_ua = request.META.get('HTTP_USER_AGENT')
5252

53-
# Check for IP address mismatch
54-
if session_ip and session_ip != current_ip:
55-
logger.warning(f"Session IP mismatch! Session IP: {session_ip}, Current IP: {current_ip}")
56-
request.session.flush()
57-
return redirect('login') # Redirect to login
58-
59-
# Check for User-Agent mismatch
60-
if session_ua and session_ua != current_ua:
61-
logger.warning(f"Session UA mismatch! Session UA: {session_ua}, Current UA: {current_ua}")
62-
request.session.flush()
63-
return redirect('login')
64-
65-
# Check for session token mismatch
66-
if session_token and session_token != request.session.session_key:
67-
logger.warning(f"Session token mismatch! Session token: {session_token}, Current session ID: {request.session.session_key}")
68-
request.session.flush()
69-
return redirect('login')
53+
# Skip hijacking checks for OAuth authentication paths to prevent redirect loops
54+
is_oauth_path = (
55+
request.path.startswith('/complete/') or
56+
request.path.startswith('/oauth/') or
57+
request.path == '/dashboard/' or
58+
'oauth' in request.path or
59+
'complete' in request.path or
60+
'azuread' in request.path
61+
)
62+
63+
# Check if user was authenticated via OAuth
64+
is_oauth_user = request.session.get('oauth_authenticated', False)
65+
66+
# Check if this is a social auth completion
67+
is_social_auth = 'social' in request.path or 'complete' in request.path
68+
69+
# If this is an OAuth path, dashboard, OAuth user, or social auth, set session data instead of checking
70+
if is_oauth_path or is_oauth_user or is_social_auth:
71+
# Set session data for OAuth users to prevent future hijacking checks
72+
request.session['ip_address'] = current_ip
73+
request.session['user_agent'] = current_ua
74+
request.session['session_token'] = request.session.session_key
75+
logger.info(f"OAuth/social auth path/user detected - setting session data for user {request.user.email}")
76+
else:
77+
# Only perform hijacking checks for non-OAuth paths
78+
# Check for IP address mismatch
79+
if session_ip and session_ip != current_ip:
80+
logger.warning(f"Session IP mismatch! Session IP: {session_ip}, Current IP: {current_ip}")
81+
request.session.flush()
82+
return redirect('login') # Redirect to login
83+
84+
# Check for User-Agent mismatch
85+
if session_ua and session_ua != current_ua:
86+
logger.warning(f"Session UA mismatch! Session UA: {session_ua}, Current UA: {current_ua}")
87+
request.session.flush()
88+
return redirect('login')
89+
90+
# Check for session token mismatch
91+
if session_token and session_token != request.session.session_key:
92+
logger.warning(f"Session token mismatch! Session token: {session_token}, Current session ID: {request.session.session_key}")
93+
request.session.flush()
94+
return redirect('login')
7095

7196
# Log the IP and accessed URL if no hijacking is detected
7297
self.log_request(request)

core/settings.py

Lines changed: 81 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -74,20 +74,25 @@
7474
RECAPTCHA_SITE_KEY = '6LesBKsrAAAAADwwja7GKS33AEC7ktIuJlcYpBDf'
7575
RECAPTCHA_SECRET_KEY = '6LesBKsrAAAAANii1CrJeF_C679-5vRMgGNC6htZ'
7676

77+
# Microsoft OAuth Client (set via environment in production)
78+
MICROSOFT_CLIENT_ID = os.getenv('MICROSOFT_CLIENT_ID', '')
79+
MICROSOFT_CLIENT_SECRET = os.getenv('MICROSOFT_CLIENT_SECRET', '')
80+
MICROSOFT_TENANT_ID = os.getenv('MICROSOFT_TENANT_ID', 'common')
81+
7782
# ---------------- Secure Session Cookie Settings ----------------
7883
# These settings ensure cookies are securely transmitted over HTTPS and protected from JS and CSRF attacks
7984
SESSION_COOKIE_SECURE = not DEBUG # Only allow HTTPS cookies in production
8085
SESSION_COOKIE_HTTPONLY = True # Prevent access to session cookies via JavaScript
81-
SESSION_COOKIE_SAMESITE = 'Strict' # Restrict cross-origin cookie sharing
86+
SESSION_COOKIE_SAMESITE = 'Lax' # Allow cross-origin cookie sharing for OAuth
8287

8388
CSRF_COOKIE_SECURE = not DEBUG # Ensure CSRF cookie is sent over HTTPS
8489
CSRF_COOKIE_SAMESITE = 'Strict' # Restrict CSRF cookie from cross-origin requests
8590

8691
# ---------------- Idle Session Timeout Configuration ----------------
8792
# Automatically logs out users after 5 minutes of inactivity, resets on every user request
88-
SESSION_COOKIE_AGE = 300 # 5 minutes in seconds
93+
SESSION_COOKIE_AGE = 1800 # 30 minutes in seconds
8994
SESSION_SAVE_EVERY_REQUEST = True # Reset the session timeout on each request
90-
SESSION_EXPIRE_AT_BROWSER_CLOSE = True # Expire session when browser closes
95+
SESSION_EXPIRE_AT_BROWSER_CLOSE = False # Keep session when browser closes
9196
SESSION_ENGINE = 'django.contrib.sessions.backends.db' # Store sessions in DB
9297

9398
# Application definition
@@ -106,10 +111,13 @@
106111
"django.contrib.staticfiles",
107112
"django_extensions",
108113
'django_cron',
109-
"django_user_agents",
110114

115+
'imagekit',
116+
"django_user_agents",
111117
'rest_framework',
112118
'drf_yasg',
119+
# Social Auth - Microsoft OAuth
120+
'social_django',
113121

114122
'home.apps.HomeConfig',
115123
'theme_pixel',
@@ -132,14 +140,24 @@
132140
"django.contrib.auth.middleware.AuthenticationMiddleware",
133141
"django.contrib.messages.middleware.MessageMiddleware",
134142
# "home.idle.IdleTimeoutMiddleware",
143+
135144
"home.idle.LogoutMiddleware",
136145
"django.middleware.clickjacking.XFrameOptionsMiddleware",
137146
"home.ratelimit_middleware.GlobalLockoutMiddleware",
138147

139148
'core.middleware.AutoLogoutMiddleware',
140149

141150
"home.admin_session_middleware.AdminSessionMiddleware", #admin session middleware
151+
152+
# "home.idle.LogoutMiddleware", # TEMPORARILY DISABLED - causing OAuth redirect issues
153+
"django.middleware.clickjacking.XFrameOptionsMiddleware",
154+
# "home.ratelimit_middleware.GlobalLockoutMiddleware", # TEMPORARILY DISABLED - causing OAuth redirect issues
155+
# "home.admin_session_middleware.AdminSessionMiddleware", # TEMPORARILY DISABLED - causing OAuth redirect issues
156+
# 'core.middleware.AutoLogoutMiddleware', # TEMPORARILY DISABLED - causing OAuth redirect issues
157+
142158
"django_user_agents.middleware.UserAgentMiddleware",
159+
# "core.middleware.LogRequestMiddleware", # TEMPORARILY DISABLED - causing OAuth redirect issues
160+
# "home.views.force_oauth_redirect_middleware", # DISABLED - causing redirect loops
143161

144162

145163
]
@@ -186,7 +204,14 @@
186204
"django.contrib.messages.context_processors.messages",
187205
'home.context_processors.dynamic_page_title',
188206
'home.context_processors.recaptcha_site_key',
207+
189208
'home.context_processors.user_scores',
209+
210+
'home.context_processors.microsoft_client_id',
211+
'social_django.context_processors.backends',
212+
'social_django.context_processors.login_redirect',
213+
214+
190215
],
191216
},
192217
},
@@ -327,7 +352,7 @@
327352

328353
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
329354

330-
LOGIN_REDIRECT_URL = '/'
355+
LOGIN_REDIRECT_URL = '/dashboard/'
331356
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
332357
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
333358
EMAIL_HOST = 'smtp.gmail.com'
@@ -352,6 +377,50 @@
352377
}
353378

354379

380+
# Authentication backends
381+
AUTHENTICATION_BACKENDS = (
382+
'django.contrib.auth.backends.ModelBackend',
383+
'home.custom_azure_backend.CustomAzureADTenantOAuth2',
384+
)
385+
386+
# Social Auth (Azure AD)
387+
SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_KEY = MICROSOFT_CLIENT_ID
388+
SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_SECRET = MICROSOFT_CLIENT_SECRET
389+
SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_TENANT_ID = MICROSOFT_TENANT_ID
390+
SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_REDIRECT_URI = 'http://localhost:8000/complete/azuread-tenant-oauth2/'
391+
SOCIAL_AUTH_REDIRECT_IS_HTTPS = False
392+
SOCIAL_AUTH_LOGIN_REDIRECT_URL = '/dashboard/'
393+
# SOCIAL_AUTH_LOGIN_ERROR_URL = '/accounts/login/' # DISABLED TO PREVENT LOGIN REDIRECTS
394+
SOCIAL_AUTH_NEW_USER_REDIRECT_URL = '/dashboard/'
395+
SOCIAL_AUTH_NEW_ASSOCIATION_REDIRECT_URL = '/dashboard/'
396+
# SOCIAL_AUTH_DISCONNECT_REDIRECT_URL = '/accounts/login/' # DISABLED TO PREVENT LOGIN REDIRECTS
397+
SOCIAL_AUTH_STRATEGY = 'social_django.strategy.DjangoStrategy'
398+
SOCIAL_AUTH_STORAGE = 'social_django.models.DjangoStorage'
399+
SOCIAL_AUTH_RAISE_EXCEPTIONS = False
400+
SOCIAL_AUTH_SANITIZE_REDIRECTS = False
401+
SOCIAL_AUTH_RAISE_EXCEPTIONS = False
402+
SOCIAL_AUTH_USER_MODEL = 'home.User'
403+
SOCIAL_AUTH_CREATE_USERS = True
404+
SOCIAL_AUTH_ASSOCIATE_BY_EMAIL = True
405+
SOCIAL_AUTH_ALWAYS_ASSOCIATE = False
406+
407+
# Ensure user is created and details saved, enforce Deakin rule
408+
SOCIAL_AUTH_PIPELINE = (
409+
'social_core.pipeline.social_auth.social_details',
410+
'social_core.pipeline.social_auth.social_uid',
411+
'social_core.pipeline.social_auth.auth_allowed',
412+
'social_core.pipeline.social_auth.social_user',
413+
'social_core.pipeline.user.get_username',
414+
'social_core.pipeline.social_auth.associate_by_email',
415+
'social_core.pipeline.user.create_user',
416+
'social_core.pipeline.social_auth.associate_user',
417+
'social_core.pipeline.social_auth.load_extra_data',
418+
'social_core.pipeline.user.user_details',
419+
'home.pipeline.check_deakin_email',
420+
'home.pipeline.set_oauth_redirect_url',
421+
'home.pipeline.ensure_user_authenticated',
422+
)
423+
355424
CACHES = {
356425
'default': {
357426
'BACKEND': 'django.core.cache.backends.memcached.PyMemcacheCache',
@@ -467,7 +536,7 @@
467536

468537
# CORS configuration
469538
CORS_ALLOWED_ORIGINS = [
470-
'http://127.0.0.1:8000', # Website localhost server url
539+
'http://localhost:8000', # Website localhost server url
471540
'https://hardhatwebdev2024.pythonanywhere.com', # Frontend url
472541
]
473542

@@ -488,9 +557,9 @@
488557

489558
# ---------------- Idle Session Timeout Configuration ----------------
490559
# Automatically logs out users after 5 minutes of inactivity, resets on every user request
491-
SESSION_COOKIE_AGE = 300 # 5 minutes in seconds
560+
SESSION_COOKIE_AGE = 1800 # 30 minutes in seconds
492561
SESSION_SAVE_EVERY_REQUEST = True # Reset the session timeout on each request
493-
SESSION_EXPIRE_AT_BROWSER_CLOSE = True # Expire session when browser closes
562+
SESSION_EXPIRE_AT_BROWSER_CLOSE = False # Keep session when browser closes
494563
SESSION_ENGINE = 'django.contrib.sessions.backends.db' # Store sessions in DB
495564

496565

@@ -961,5 +1030,9 @@
9611030
# DATA_UPLOAD_MAX_NUMBER_FIELDS = 1000
9621031
# DATA_UPLOAD_MAX_MEMORY_SIZE = 10485760 # 10 MB
9631032

1033+
DEBUG = True
1034+
1035+
1036+
9641037
SECURITY_EMAIL = "hardhatwebsite@gmail.com"
9651038

0 commit comments

Comments
 (0)