Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@
RECAPTCHA_SITE_KEY = '6LesBKsrAAAAADwwja7GKS33AEC7ktIuJlcYpBDf'
RECAPTCHA_SECRET_KEY = '6LesBKsrAAAAANii1CrJeF_C679-5vRMgGNC6htZ'

# Microsoft OAuth Client (set via environment in production)
MICROSOFT_CLIENT_ID = os.getenv('MICROSOFT_CLIENT_ID', '')
MICROSOFT_CLIENT_SECRET = os.getenv('MICROSOFT_CLIENT_SECRET', '')
MICROSOFT_TENANT_ID = os.getenv('MICROSOFT_TENANT_ID', 'common')

# ---------------- Secure Session Cookie Settings ----------------
# These settings ensure cookies are securely transmitted over HTTPS and protected from JS and CSRF attacks
SESSION_COOKIE_SECURE = not DEBUG # Only allow HTTPS cookies in production
Expand Down Expand Up @@ -176,6 +181,7 @@
"django.contrib.messages.context_processors.messages",
'home.context_processors.dynamic_page_title',
'home.context_processors.recaptcha_site_key',
'home.context_processors.microsoft_client_id',


],
Expand Down
13 changes: 10 additions & 3 deletions home/context_processors.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
# page_title = "Home"
# else:
# page_title = capfirst(path)

#
# # Add your site name to the title
# full_title = f"{page_title} - Hardhat Enterprises"
# return {"dynamic_title": full_title}
Expand All @@ -24,9 +24,9 @@
# # page_title = custom_titles.get(path, capfirst(path.strip("/").replace("-", " ").replace("_", " ")))
# # if not page_title or page_title == "Index":
# # page_title = "Home"

#
# # full_title = f"{page_title} - Hardhat Enterprises"
# # return {"dynamic_title": full_title}
# return {"dynamic_title": full_title}

# def recaptcha_site_key(request):
# return {
Expand Down Expand Up @@ -86,3 +86,10 @@ def dynamic_page_title(request):

def recaptcha_site_key(request):
return {'RECAPTCHA_SITE_KEY': settings.RECAPTCHA_SITE_KEY}

def microsoft_client_id(request):
return {
'MICROSOFT_CLIENT_ID': getattr(settings, 'MICROSOFT_CLIENT_ID', ''),
'MICROSOFT_TENANT_ID': getattr(settings, 'MICROSOFT_TENANT_ID', 'deakin.edu.au'),
'DEAKIN_DOMAIN': 'deakin.edu.au'
}
443 changes: 322 additions & 121 deletions home/templates/accounts/sign-in.html

Large diffs are not rendered by default.

12 changes: 11 additions & 1 deletion home/templates/layouts/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
https://buttons.github.io https://unpkg.com https://code.jquery.com https://cdn.jsdelivr.net https://stackpath.bootstrapcdn.com https://www.google.com https://www.gstatic.com;
style-src 'self' {{ request.scheme }}://{{ request.get_host }}/ 'unsafe-inline' https://fonts.googleapis.com https://cdn.jsdelivr.net;
font-src 'self' https://fonts.gstatic.com;
frame-src 'self' https://www.google.com;
frame-src 'self' https://www.google.com https://accounts.google.com;
img-src 'self' data:;
">

Expand Down Expand Up @@ -306,6 +306,16 @@
}
</style>

<!-- Google Identity Services - Load only when needed -->
<script>
window.GOOGLE_CLIENT_ID = '{{ GOOGLE_OAUTH_CLIENT_ID }}';
window.GOOGLE_LOADED = false;
window.GOOGLE_LOAD_ERROR = false;
window.MICROSOFT_CLIENT_ID = '{{ MICROSOFT_CLIENT_ID }}';
window.MICROSOFT_TENANT_ID = '{{ MICROSOFT_TENANT_ID }}';
window.DEAKIN_DOMAIN = '{{ DEAKIN_DOMAIN }}';
</script>

</head>

<body>
Expand Down
2 changes: 2 additions & 0 deletions home/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@
path("verifyEmail/", views.VerifyOTP, name="verifyEmail"),
path('accounts/login/', views.login_with_otp, name='login_with_otp'),
path('accounts/verify-otp/', views.verify_otp, name='verify_otp'),
path('accounts/microsoft-login/', views.microsoft_login, name='microsoft_login'),
path('accounts/microsoft-callback/', views.microsoft_callback, name='microsoft_callback'),
# Statistics
path('chart/filter-options', views.get_filter_options, name='chart-filter-options'),
path('chart/project-priority/<str:priority>', views.get_priority_breakdown, name='chart-filter-options'),
Expand Down
127 changes: 127 additions & 0 deletions home/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@
from django.core.paginator import Paginator
from .models import BlogPost
from django.template.loader import render_to_string
import msal
import requests

#from .models import Student, Project, Progress

Expand Down Expand Up @@ -1216,6 +1218,131 @@ def get_client_ip(request):
return x_forwarded_for.split(',')[0].strip()
return request.META.get('REMOTE_ADDR')


@require_POST
@csrf_protect
def microsoft_login(request):
try:
data = json.loads(request.body.decode('utf-8'))
access_token = data.get('access_token')
if not access_token:
return JsonResponse({'error': 'Missing access token'}, status=400)

# Verify token with Microsoft Graph API
graph_url = 'https://graph.microsoft.com/v1.0/me'
headers = {
'Authorization': f'Bearer {access_token}',
'Content-Type': 'application/json'
}

response = requests.get(graph_url, headers=headers)
if response.status_code != 200:
return JsonResponse({'error': 'Invalid access token'}, status=401)

user_info = response.json()
user_email = user_info.get('mail') or user_info.get('userPrincipalName')
if not user_email:
return JsonResponse({'error': 'Email not present in token'}, status=400)

# Validate that the user is from Deakin University
if not user_email.endswith('@deakin.edu.au'):
return JsonResponse({
'error': 'Access restricted to Deakin University students and staff. Please use a @deakin.edu.au email address.'
}, status=403)

first_name = user_info.get('givenName', '')
last_name = user_info.get('surname', '')
display_name = user_info.get('displayName', f'{first_name} {last_name}'.strip())

# Extract additional Deakin-specific information
job_title = user_info.get('jobTitle', '')
department = user_info.get('department', '')
office_location = user_info.get('officeLocation', '')

UserModel = get_user_model()
user, created = UserModel.objects.get_or_create(
email=user_email,
defaults={
'first_name': first_name,
'last_name': last_name,
'username': user_email.split('@')[0], # Use email prefix as username
'is_active': True,
'is_verified': True,
'is_staff': job_title and ('staff' in job_title.lower() or 'lecturer' in job_title.lower() or 'professor' in job_title.lower()),
}
)

if created:
# Set an unusable password for social-only accounts
user.set_unusable_password()
user.save()

# Log successful Deakin user registration
if settings.DEBUG:
print(f'New Deakin user registered: {user_email} - {display_name}')
else:
# Update existing user information
user.first_name = first_name
user.last_name = last_name
user.is_verified = True
user.save(update_fields=['first_name', 'last_name', 'is_verified'])

login(request, user)

# Track login metadata
try:
user.last_login_ip = get_client_ip(request)
user.last_login_browser = request.META.get('HTTP_USER_AGENT', '')[:256]
user.save(update_fields=['last_login_ip', 'last_login_browser'])
except Exception:
pass

# Log successful Deakin authentication
if settings.DEBUG:
print(f'Deakin user authenticated: {user_email} - {display_name}')

# Use the same redirect logic as regular login
redirect_url = get_login_redirect_url(user)

return JsonResponse({'redirect_url': redirect_url})

except ValueError:
return JsonResponse({'error': 'Invalid request body'}, status=400)
except Exception as e:
if settings.DEBUG:
print('Microsoft login error:', str(e))
return JsonResponse({'error': 'Authentication failed'}, status=401)


def microsoft_callback(request):
"""
Handle Microsoft OAuth callback and redirect to appropriate page
"""
try:
# Get the authorization code from the callback
code = request.GET.get('code')
error = request.GET.get('error')

if error:
messages.error(request, f'Microsoft authentication failed: {error}')
return redirect('login')

if not code:
messages.error(request, 'No authorization code received from Microsoft')
return redirect('login')

# Exchange code for access token (this would normally be done server-side)
# For now, we'll redirect to the login page with a message
messages.info(request, 'Microsoft authentication successful! Please complete the login process.')
return redirect('login')

except Exception as e:
if settings.DEBUG:
print('Microsoft callback error:', str(e))
messages.error(request, 'Microsoft authentication callback failed')
return redirect('login')


def login_with_tracking(request):
if request.method == 'POST':
username = request.POST.get('username')
Expand Down
Binary file modified locale/es/LC_MESSAGES/django.mo
Binary file not shown.
Loading