fixed some thangs, implemented cloudflare turnstile

This commit is contained in:
pacnpal
2024-10-29 13:31:30 -04:00
parent db991bd023
commit 0b773e7435
17 changed files with 234 additions and 33 deletions

28
accounts/mixins.py Normal file
View File

@@ -0,0 +1,28 @@
import requests
from django.conf import settings
from django.core.exceptions import ValidationError
class TurnstileMixin:
"""
Mixin to handle Cloudflare Turnstile validation.
"""
def validate_turnstile(self, request):
"""
Validate the Turnstile response token.
"""
token = request.POST.get('cf-turnstile-response')
if not token:
raise ValidationError('Please complete the Turnstile challenge.')
# Verify the token with Cloudflare
data = {
'secret': settings.TURNSTILE_SECRET_KEY,
'response': token,
'remoteip': request.META.get('REMOTE_ADDR'),
}
response = requests.post(settings.TURNSTILE_VERIFY_URL, data=data)
result = response.json()
if not result.get('success'):
raise ValidationError('Turnstile validation failed. Please try again.')

View File

View File

@@ -0,0 +1,14 @@
from django import template
from django.conf import settings
register = template.Library()
@register.inclusion_tag('accounts/turnstile_widget.html')
def turnstile_widget():
"""
Template tag to render the Cloudflare Turnstile widget.
Usage: {% load turnstile_tags %}{% turnstile_widget %}
"""
return {
'site_key': settings.TURNSTILE_SITE_KEY
}

View File

@@ -1,13 +1,17 @@
from django.urls import path from django.urls import path
from django.contrib.auth import views as auth_views from django.contrib.auth import views as auth_views
from allauth.account.views import LogoutView
from . import views from . import views
app_name = 'accounts' app_name = 'accounts'
urlpatterns = [ urlpatterns = [
# Override allauth's login and signup views with our Turnstile-enabled versions
path('login/', views.CustomLoginView.as_view(), name='account_login'),
path('signup/', views.CustomSignupView.as_view(), name='account_signup'),
# Authentication views # Authentication views
path('login/', auth_views.LoginView.as_view(template_name='accounts/login.html'), name='login'), path('logout/', LogoutView.as_view(), name='logout'),
path('logout/', auth_views.LogoutView.as_view(), name='logout'),
path('password_change/', auth_views.PasswordChangeView.as_view(), name='password_change'), path('password_change/', auth_views.PasswordChangeView.as_view(), name='password_change'),
path('password_change/done/', auth_views.PasswordChangeDoneView.as_view(), name='password_change_done'), path('password_change/done/', auth_views.PasswordChangeDoneView.as_view(), name='password_change_done'),
path('password_reset/', auth_views.PasswordResetView.as_view(), name='password_reset'), path('password_reset/', auth_views.PasswordResetView.as_view(), name='password_reset'),

View File

@@ -4,6 +4,7 @@ from django.shortcuts import get_object_or_404, redirect, render
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib import messages from django.contrib import messages
from django.core.exceptions import ValidationError
from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter
from allauth.socialaccount.providers.discord.views import DiscordOAuth2Adapter from allauth.socialaccount.providers.discord.views import DiscordOAuth2Adapter
from allauth.socialaccount.providers.oauth2.client import OAuth2Client from allauth.socialaccount.providers.oauth2.client import OAuth2Client
@@ -20,9 +21,29 @@ from django.urls import reverse
from accounts.models import User, PasswordReset from accounts.models import User, PasswordReset
from reviews.models import Review from reviews.models import Review
from email_service.services import EmailService from email_service.services import EmailService
from allauth.account.views import LoginView, SignupView
from .mixins import TurnstileMixin
User = get_user_model() User = get_user_model()
class CustomLoginView(TurnstileMixin, LoginView):
def form_valid(self, form):
try:
self.validate_turnstile(self.request)
except ValidationError as e:
form.add_error(None, str(e))
return self.form_invalid(form)
return super().form_valid(form)
class CustomSignupView(TurnstileMixin, SignupView):
def form_valid(self, form):
try:
self.validate_turnstile(self.request)
except ValidationError as e:
form.add_error(None, str(e))
return self.form_invalid(form)
return super().form_valid(form)
@login_required @login_required
def user_redirect_view(request): def user_redirect_view(request):
"""Redirect /user/ to the logged-in user's profile""" """Redirect /user/ to the logged-in user's profile"""

View File

@@ -55,6 +55,23 @@
@apply mt-2 text-sm text-red-600 dark:text-red-400; @apply mt-2 text-sm text-red-600 dark:text-red-400;
} }
/* Status Badge Styles */
.status-badge {
@apply inline-flex items-center px-3 py-1 text-sm font-medium rounded-full;
}
.status-operating {
@apply text-green-800 bg-green-100 dark:bg-green-800/30 dark:text-green-100 dark:ring-1 dark:ring-green-400/30;
}
.status-closed {
@apply text-red-800 bg-red-100 dark:bg-red-800/30 dark:text-red-100 dark:ring-1 dark:ring-red-400/30;
}
.status-construction {
@apply text-yellow-800 bg-yellow-100 dark:bg-yellow-800/30 dark:text-yellow-100 dark:ring-1 dark:ring-yellow-400/30;
}
/* Auth Card Styles */ /* Auth Card Styles */
.auth-card { .auth-card {
@apply w-full max-w-md p-8 mx-auto border shadow-xl bg-white/90 dark:bg-gray-800/90 rounded-2xl backdrop-blur-sm border-gray-200/50 dark:border-gray-700/50; @apply w-full max-w-md p-8 mx-auto border shadow-xl bg-white/90 dark:bg-gray-800/90 rounded-2xl backdrop-blur-sm border-gray-200/50 dark:border-gray-700/50;

View File

@@ -1749,6 +1749,72 @@ select {
color: rgb(248 113 113 / var(--tw-text-opacity)); color: rgb(248 113 113 / var(--tw-text-opacity));
} }
/* Status Badge Styles */
.status-badge {
display: inline-flex;
align-items: center;
border-radius: 9999px;
padding-left: 0.75rem;
padding-right: 0.75rem;
padding-top: 0.25rem;
padding-bottom: 0.25rem;
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 500;
}
.status-operating {
--tw-bg-opacity: 1;
background-color: rgb(220 252 231 / var(--tw-bg-opacity));
--tw-text-opacity: 1;
color: rgb(22 101 52 / var(--tw-text-opacity));
}
.status-operating:is(.dark *) {
background-color: rgb(22 101 52 / 0.3);
--tw-text-opacity: 1;
color: rgb(220 252 231 / var(--tw-text-opacity));
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
--tw-ring-color: rgb(74 222 128 / 0.3);
}
.status-closed {
--tw-bg-opacity: 1;
background-color: rgb(254 226 226 / var(--tw-bg-opacity));
--tw-text-opacity: 1;
color: rgb(153 27 27 / var(--tw-text-opacity));
}
.status-closed:is(.dark *) {
background-color: rgb(153 27 27 / 0.3);
--tw-text-opacity: 1;
color: rgb(254 226 226 / var(--tw-text-opacity));
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
--tw-ring-color: rgb(248 113 113 / 0.3);
}
.status-construction {
--tw-bg-opacity: 1;
background-color: rgb(254 249 195 / var(--tw-bg-opacity));
--tw-text-opacity: 1;
color: rgb(133 77 14 / var(--tw-text-opacity));
}
.status-construction:is(.dark *) {
background-color: rgb(133 77 14 / 0.3);
--tw-text-opacity: 1;
color: rgb(254 249 195 / var(--tw-text-opacity));
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
--tw-ring-color: rgb(250 204 21 / 0.3);
}
/* Auth Card Styles */ /* Auth Card Styles */
.auth-card { .auth-card {
@@ -2513,6 +2579,10 @@ select {
background-color: rgb(254 249 195 / 0.9); background-color: rgb(254 249 195 / 0.9);
} }
.bg-white\/80 {
background-color: rgb(255 255 255 / 0.8);
}
.bg-gradient-to-br { .bg-gradient-to-br {
background-image: linear-gradient(to bottom right, var(--tw-gradient-stops)); background-image: linear-gradient(to bottom right, var(--tw-gradient-stops));
} }
@@ -2972,6 +3042,11 @@ select {
background-color: rgb(79 70 229 / 0.1); background-color: rgb(79 70 229 / 0.1);
} }
.hover\:bg-gray-200:hover {
--tw-bg-opacity: 1;
background-color: rgb(229 231 235 / var(--tw-bg-opacity));
}
.hover\:from-primary\/90:hover { .hover\:from-primary\/90:hover {
--tw-gradient-from: rgb(79 70 229 / 0.9) var(--tw-gradient-from-position); --tw-gradient-from: rgb(79 70 229 / 0.9) var(--tw-gradient-from-position);
--tw-gradient-to: rgb(79 70 229 / 0) var(--tw-gradient-to-position); --tw-gradient-to: rgb(79 70 229 / 0) var(--tw-gradient-to-position);
@@ -3125,6 +3200,10 @@ select {
background-color: rgb(133 77 14 / 0.3); background-color: rgb(133 77 14 / 0.3);
} }
.dark\:bg-gray-700\/80:is(.dark *) {
background-color: rgb(55 65 81 / 0.8);
}
.dark\:from-gray-950:is(.dark *) { .dark\:from-gray-950:is(.dark *) {
--tw-gradient-from: #030712 var(--tw-gradient-from-position); --tw-gradient-from: #030712 var(--tw-gradient-from-position);
--tw-gradient-to: rgb(3 7 18 / 0) var(--tw-gradient-to-position); --tw-gradient-to: rgb(3 7 18 / 0) var(--tw-gradient-to-position);
@@ -3223,6 +3302,11 @@ select {
background-color: rgb(79 70 229 / 0.2); background-color: rgb(79 70 229 / 0.2);
} }
.dark\:hover\:bg-gray-600:hover:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(75 85 99 / var(--tw-bg-opacity));
}
.dark\:hover\:text-blue-300:hover:is(.dark *) { .dark\:hover\:text-blue-300:hover:is(.dark *) {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(147 197 253 / var(--tw-text-opacity)); color: rgb(147 197 253 / var(--tw-text-opacity));

View File

@@ -2,6 +2,7 @@
{% load i18n %} {% load i18n %}
{% load account socialaccount %} {% load account socialaccount %}
{% load static %} {% load static %}
{% load turnstile_tags %}
{% block title %}Login - ThrillWiki{% endblock %} {% block title %}Login - ThrillWiki{% endblock %}
@@ -88,6 +89,8 @@
</div> </div>
</div> </div>
{% turnstile_widget %}
{% if redirect_field_value %} {% if redirect_field_value %}
<input type="hidden" name="{{ redirect_field_name }}" value="{{ redirect_field_value }}"> <input type="hidden" name="{{ redirect_field_name }}" value="{{ redirect_field_value }}">
{% endif %} {% endif %}

View File

@@ -2,6 +2,7 @@
{% load i18n %} {% load i18n %}
{% load account socialaccount %} {% load account socialaccount %}
{% load static %} {% load static %}
{% load turnstile_tags %}
{% block title %}Register - ThrillWiki{% endblock %} {% block title %}Register - ThrillWiki{% endblock %}
@@ -116,6 +117,8 @@
{% endif %} {% endif %}
</div> </div>
{% turnstile_widget %}
{% if redirect_field_value %} {% if redirect_field_value %}
<input type="hidden" name="{{ redirect_field_name }}" value="{{ redirect_field_value }}"> <input type="hidden" name="{{ redirect_field_name }}" value="{{ redirect_field_value }}">
{% endif %} {% endif %}

View File

@@ -0,0 +1,2 @@
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<div class="cf-turnstile" data-sitekey="{{ site_key }}" data-theme="light"></div>

View File

@@ -1,6 +1,6 @@
{% load static %} {% load static %}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" class="no-js"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -11,9 +11,14 @@
<!-- Prevent flash of wrong theme --> <!-- Prevent flash of wrong theme -->
<script> <script>
// Immediately remove 'no-js' class and add initial theme class // Get theme from localStorage or system preference
document.documentElement.classList.remove('no-js'); let theme = localStorage.getItem('theme');
if (localStorage.theme === 'dark' || (!localStorage.theme && window.matchMedia('(prefers-color-scheme: dark)').matches)) { if (!theme) {
theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
localStorage.setItem('theme', theme);
}
// Apply theme immediately before page loads
if (theme === 'dark') {
document.documentElement.classList.add('dark'); document.documentElement.classList.add('dark');
} }
</script> </script>
@@ -49,30 +54,44 @@
this.setTheme(e.matches ? 'dark' : 'light'); this.setTheme(e.matches ? 'dark' : 'light');
} }
}); });
// Set initial theme icon state
this.updateThemeIcon();
}); });
}, },
setTheme(theme) { setTheme(theme) {
// Force a repaint by temporarily adding a class
document.documentElement.classList.add('theme-transitioning');
if (theme === 'dark') { if (theme === 'dark') {
document.documentElement.classList.add('dark'); document.documentElement.classList.add('dark');
} else { } else {
document.documentElement.classList.remove('dark'); document.documentElement.classList.remove('dark');
} }
localStorage.theme = theme; localStorage.setItem('theme', theme);
this.updateThemeIcon();
// Remove the transition class after a short delay
setTimeout(() => {
document.documentElement.classList.remove('theme-transitioning');
}, 100);
}, },
toggleTheme() { toggleTheme() {
const isDark = document.documentElement.classList.contains('dark'); const isDark = document.documentElement.classList.contains('dark');
this.setTheme(isDark ? 'light' : 'dark'); this.setTheme(isDark ? 'light' : 'dark');
},
updateThemeIcon() {
const isDark = document.documentElement.classList.contains('dark');
const sunIcon = document.querySelector('#theme-toggle .fa-sun');
const moonIcon = document.querySelector('#theme-toggle .fa-moon');
if (sunIcon && moonIcon) {
// Show sun icon in dark mode (to indicate you can switch to light)
// Show moon icon in light mode (to indicate you can switch to dark)
if (isDark) {
sunIcon.classList.remove('hidden');
moonIcon.classList.add('hidden');
} else {
sunIcon.classList.add('hidden');
moonIcon.classList.remove('hidden');
}
}
} }
}; };
@@ -81,24 +100,24 @@
</script> </script>
<style> <style>
/* Prevent flash of wrong theme */
.no-js {
visibility: hidden;
}
/* Smooth theme transitions */ /* Smooth theme transitions */
:root { :root {
--theme-transition-duration: 200ms; --theme-transition-duration: 200ms;
} }
.theme-transitioning * {
transition: none !important;
}
body { body {
transition: background-color var(--theme-transition-duration) ease-in-out, transition: background-color var(--theme-transition-duration) ease-in-out,
color var(--theme-transition-duration) ease-in-out; color var(--theme-transition-duration) ease-in-out;
} }
/* Ensure theme toggle button has consistent size */
#theme-toggle {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
}
</style> </style>
</head> </head>
<body class="flex flex-col min-h-screen text-gray-900 bg-gradient-to-br from-white via-blue-50 to-indigo-50 dark:from-gray-950 dark:via-indigo-950 dark:to-purple-950 dark:text-white"> <body class="flex flex-col min-h-screen text-gray-900 bg-gradient-to-br from-white via-blue-50 to-indigo-50 dark:from-gray-950 dark:via-indigo-950 dark:to-purple-950 dark:text-white">
@@ -141,9 +160,9 @@
<!-- Right Side Menu --> <!-- Right Side Menu -->
<div class="flex items-center space-x-6"> <div class="flex items-center space-x-6">
<!-- Theme Toggle --> <!-- Theme Toggle -->
<button id="theme-toggle" class="p-2.5 rounded-lg bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 hover:bg-primary/10 dark:hover:bg-primary/20 transition-all shadow-sm border border-gray-200/50 dark:border-gray-600/50"> <button id="theme-toggle" class="inline-flex items-center justify-center p-2 text-gray-500 transition-all border border-gray-200 rounded-lg bg-white/80 dark:bg-gray-700/80 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-600 dark:border-gray-600">
<i class="hidden text-lg fas fa-sun dark:inline"></i> <i class="hidden text-lg fas fa-sun"></i>
<i class="text-lg fas fa-moon dark:hidden"></i> <i class="hidden text-lg fas fa-moon"></i>
</button> </button>
<!-- User Menu --> <!-- User Menu -->

View File

@@ -206,4 +206,9 @@ AUTH_USER_MODEL = 'accounts.User'
# Tailwind configuration # Tailwind configuration
TAILWIND_CLI_CONFIG_FILE = os.path.join(BASE_DIR, 'tailwind.config.js') TAILWIND_CLI_CONFIG_FILE = os.path.join(BASE_DIR, 'tailwind.config.js')
TAILWIND_CLI_SRC_CSS = os.path.join(BASE_DIR, 'assets/css/src/input.css') TAILWIND_CLI_SRC_CSS = os.path.join(BASE_DIR, 'assets/css/src/input.css')
TAILWIND_CLI_DIST_CSS = os.path.join(BASE_DIR, 'static/css/tailwind.css') TAILWIND_CLI_DIST_CSS = os.path.join(BASE_DIR, 'static/css/tailwind.css')
# Cloudflare Turnstile settings
TURNSTILE_SITE_KEY = '0x4AAAAAAAyqVp3RjccrC9Kz'
TURNSTILE_SECRET_KEY = '0x4AAAAAAAyqVrQolYsrAFGJ39PXHJ_HQzY'
TURNSTILE_VERIFY_URL = 'https://challenges.cloudflare.com/turnstile/v0/siteverify'

View File

@@ -19,8 +19,12 @@ urlpatterns = [
path('terms/', TemplateView.as_view(template_name='pages/terms.html'), name='terms'), path('terms/', TemplateView.as_view(template_name='pages/terms.html'), name='terms'),
path('privacy/', TemplateView.as_view(template_name='pages/privacy.html'), name='privacy'), path('privacy/', TemplateView.as_view(template_name='pages/privacy.html'), name='privacy'),
# Authentication URLs # Custom authentication URLs first (to override allauth defaults)
path('accounts/', include('allauth.urls')), # This includes social auth URLs path('accounts/', include('accounts.urls')),
# Default allauth URLs (for social auth and other features)
path('accounts/', include('allauth.urls')),
path('accounts/email-required/', accounts_views.email_required, name='email_required'), path('accounts/email-required/', accounts_views.email_required, name='email_required'),
# User profile URLs # User profile URLs
@@ -31,9 +35,6 @@ urlpatterns = [
# Redirect /user/ to the user's profile if logged in # Redirect /user/ to the user's profile if logged in
path('user/', accounts_views.user_redirect_view, name='user_redirect'), path('user/', accounts_views.user_redirect_view, name='user_redirect'),
# Include remaining accounts URLs
path('', include('accounts.urls')),
] ]
# Serve static files in development # Serve static files in development