This commit is contained in:
Iliyan Angelov
2025-11-26 22:32:20 +02:00
commit ed94dd22dd
150 changed files with 14058 additions and 0 deletions

0
accounts/__init__.py Normal file
View File

47
accounts/admin.py Normal file
View File

@@ -0,0 +1,47 @@
"""
Admin configuration for accounts app.
"""
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from .models import User, UserProfile, ActivityLog, FailedLoginAttempt
@admin.register(User)
class UserAdmin(BaseUserAdmin):
"""Custom user admin."""
list_display = ('username', 'email', 'role', 'is_verified', 'is_active', 'created_at')
list_filter = ('role', 'is_verified', 'is_active', 'created_at')
fieldsets = BaseUserAdmin.fieldsets + (
('Additional Info', {'fields': ('role', 'is_verified', 'mfa_enabled', 'last_login_ip')}),
)
add_fieldsets = BaseUserAdmin.add_fieldsets + (
('Additional Info', {'fields': ('role', 'is_verified')}),
)
@admin.register(UserProfile)
class UserProfileAdmin(admin.ModelAdmin):
"""User profile admin."""
list_display = ('user', 'first_name', 'last_name', 'consent_given', 'preferred_language')
list_filter = ('consent_given', 'preferred_language')
search_fields = ('user__username', 'user__email', 'first_name', 'last_name')
@admin.register(ActivityLog)
class ActivityLogAdmin(admin.ModelAdmin):
"""Activity log admin."""
list_display = ('user', 'action', 'ip_address', 'timestamp')
list_filter = ('action', 'timestamp')
search_fields = ('user__username', 'ip_address')
readonly_fields = ('user', 'action', 'ip_address', 'user_agent', 'details', 'timestamp')
date_hierarchy = 'timestamp'
@admin.register(FailedLoginAttempt)
class FailedLoginAttemptAdmin(admin.ModelAdmin):
"""Failed login attempt admin."""
list_display = ('email_or_username', 'ip_address', 'timestamp', 'is_blocked')
list_filter = ('is_blocked', 'timestamp')
search_fields = ('email_or_username', 'ip_address')
readonly_fields = ('email_or_username', 'ip_address', 'user_agent', 'timestamp')
date_hierarchy = 'timestamp'

6
accounts/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class AccountsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'accounts'

141
accounts/form_mixins.py Normal file
View File

@@ -0,0 +1,141 @@
"""
Form mixins for bot protection and validation.
"""
from django import forms
from django.core.cache import cache
from django.utils import timezone
from datetime import timedelta
import time
class HoneypotMixin:
"""
Honeypot field mixin - adds a hidden field that bots will fill but humans won't.
"""
# This field should be left empty by real users
website = forms.CharField(
required=False,
widget=forms.HiddenInput(attrs={'tabindex': '-1', 'autocomplete': 'off'}),
label='', # Empty label so screen readers skip it
)
def clean_website(self):
"""If this field is filled, it's likely a bot."""
website = self.cleaned_data.get('website')
if website:
raise forms.ValidationError('Bot detected. Please try again.')
return website
class TimeBasedValidationMixin:
"""
Time-based validation - prevents forms from being submitted too quickly (bot behavior).
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Add a hidden timestamp field
self.fields['form_timestamp'] = forms.CharField(
required=False,
widget=forms.HiddenInput(),
initial=str(time.time())
)
def clean_form_timestamp(self):
"""Validate that form wasn't submitted too quickly."""
timestamp = self.cleaned_data.get('form_timestamp')
if not timestamp:
# If timestamp is missing, it might be a bot
raise forms.ValidationError('Invalid form submission.')
try:
submit_time = float(timestamp)
current_time = time.time()
elapsed = current_time - submit_time
# Forms submitted in less than 2 seconds are likely bots
if elapsed < 2:
raise forms.ValidationError('Form submitted too quickly. Please take your time.')
# Forms submitted after 1 hour are likely stale
if elapsed > 3600:
raise forms.ValidationError('Form session expired. Please refresh and try again.')
except (ValueError, TypeError):
raise forms.ValidationError('Invalid form submission.')
return timestamp
class RateLimitMixin:
"""
Rate limiting mixin - prevents too many form submissions from the same IP/user.
"""
def __init__(self, *args, **kwargs):
self.request = kwargs.pop('request', None)
super().__init__(*args, **kwargs)
def clean(self):
cleaned_data = super().clean()
if self.request:
# Get client IP
ip = self.get_client_ip(self.request)
# Create a unique key for this form type
form_name = self.__class__.__name__
cache_key = f'form_submission_{form_name}_{ip}'
# Check submission count
submissions = cache.get(cache_key, 0)
# Limit: 10 submissions per hour per IP
if submissions >= 10:
raise forms.ValidationError(
'Too many submissions. Please wait before submitting again.'
)
# Increment counter
cache.set(cache_key, submissions + 1, 3600) # 1 hour
return cleaned_data
def get_client_ip(self, request):
"""Get client IP address."""
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('REMOTE_ADDR')
return ip
class BrowserFingerprintMixin:
"""
Browser fingerprint validation - ensures form is submitted from a real browser.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['user_agent_hash'] = forms.CharField(
required=False,
widget=forms.HiddenInput()
)
def clean_user_agent_hash(self):
"""Validate user agent is present and reasonable."""
ua_hash = self.cleaned_data.get('user_agent_hash')
# If no user agent hash, it might be a bot
if not ua_hash:
raise forms.ValidationError('Invalid browser signature.')
return ua_hash
class BotProtectionMixin(HoneypotMixin, TimeBasedValidationMixin, RateLimitMixin):
"""
Combined bot protection mixin that includes:
- Honeypot field
- Time-based validation
- Rate limiting
"""
pass

138
accounts/forms.py Normal file
View File

@@ -0,0 +1,138 @@
"""
Forms for accounts app.
"""
from django import forms
from django.contrib.auth.forms import UserCreationForm
from django_otp.plugins.otp_totp.models import TOTPDevice
from django.utils import timezone
from .models import User, UserProfile
from .security import InputSanitizer, PasswordSecurity
from .form_mixins import BotProtectionMixin
class UserRegistrationForm(BotProtectionMixin, UserCreationForm):
"""User registration form with security validation and bot protection."""
email = forms.EmailField(required=True)
consent_given = forms.BooleanField(
required=True,
label='I agree to the Privacy Policy and Terms of Service'
)
class Meta:
model = User
fields = ('username', 'email', 'password1', 'password2', 'consent_given')
def clean_username(self):
username = self.cleaned_data.get('username')
if username:
# Sanitize username
username = InputSanitizer.sanitize_html(username)
# Check for SQL injection patterns
sanitized = InputSanitizer.sanitize_sql(username)
if sanitized is None:
raise forms.ValidationError('Invalid username format.')
return username
def clean_email(self):
email = self.cleaned_data.get('email')
if email:
# Validate email format
if not InputSanitizer.validate_email(email):
raise forms.ValidationError('Invalid email format.')
# Sanitize email
email = InputSanitizer.sanitize_html(email)
return email
def clean_password1(self):
password = self.cleaned_data.get('password1')
if password:
is_strong, message = PasswordSecurity.check_password_strength(password)
if not is_strong:
raise forms.ValidationError(message)
return password
def save(self, commit=True):
user = super().save(commit=False)
user.email = self.cleaned_data['email']
if commit:
user.save()
# Create profile with consent
profile = UserProfile.objects.create(
user=user,
consent_given=self.cleaned_data['consent_given']
)
return user
class UserProfileForm(forms.ModelForm):
"""User profile edit form."""
first_name = forms.CharField(max_length=100, required=False)
last_name = forms.CharField(max_length=100, required=False)
class Meta:
model = UserProfile
fields = ('first_name', 'last_name', 'phone', 'preferred_language')
widgets = {
'phone': forms.TextInput(attrs={'placeholder': '+359...'}),
}
class MFAVerifyForm(forms.Form):
"""MFA verification form."""
token = forms.CharField(
max_length=6,
min_length=6,
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': '000000',
'autofocus': True,
'pattern': '[0-9]{6}',
'maxlength': '6'
}),
label='Verification Code',
help_text='Enter the 6-digit code from your authenticator app'
)
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user', None)
super().__init__(*args, **kwargs)
def verify_token(self):
"""Verify the TOTP token."""
if not self.user:
return False
token = self.cleaned_data.get('token')
if not token:
return False
# Get the TOTP device (for login, use confirmed device; for setup, use unconfirmed)
try:
# Try confirmed device first (for login)
device = TOTPDevice.objects.get(user=self.user, name='default', confirmed=True)
except TOTPDevice.DoesNotExist:
# Try unconfirmed device (for setup)
try:
device = TOTPDevice.objects.get(user=self.user, name='default', confirmed=False)
except TOTPDevice.DoesNotExist:
return False
return device.verify_token(token)
class MFASetupForm(forms.Form):
"""MFA setup form (for confirmation)."""
token = forms.CharField(
max_length=6,
min_length=6,
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': '000000',
'autofocus': True,
'pattern': '[0-9]{6}',
'maxlength': '6'
}),
label='Verification Code',
help_text='Enter the 6-digit code from your authenticator app to confirm setup'
)

View File

View File

View File

@@ -0,0 +1,110 @@
"""
Management command to check security settings and vulnerabilities.
"""
from django.core.management.base import BaseCommand
from django.conf import settings
from django.contrib.auth import get_user_model
from accounts.models import FailedLoginAttempt, ActivityLog
from django.utils import timezone
from datetime import timedelta
User = get_user_model()
class Command(BaseCommand):
help = 'Check security settings and report potential vulnerabilities'
def handle(self, *args, **options):
self.stdout.write(self.style.SUCCESS('=' * 60))
self.stdout.write(self.style.SUCCESS('Security Audit Report'))
self.stdout.write(self.style.SUCCESS('=' * 60))
issues = []
warnings = []
# Check DEBUG mode
if settings.DEBUG:
warnings.append('DEBUG mode is enabled - disable in production!')
else:
self.stdout.write(self.style.SUCCESS('✓ DEBUG mode is disabled'))
# Check SECRET_KEY
if settings.SECRET_KEY == 'django-insecure-change-this-in-production':
issues.append('CRITICAL: Default SECRET_KEY is being used!')
else:
self.stdout.write(self.style.SUCCESS('✓ SECRET_KEY is set'))
# Check ALLOWED_HOSTS
if not settings.ALLOWED_HOSTS:
issues.append('ALLOWED_HOSTS is empty - set in production!')
else:
self.stdout.write(self.style.SUCCESS(f'✓ ALLOWED_HOSTS: {settings.ALLOWED_HOSTS}'))
# Check HTTPS settings
if not settings.DEBUG:
if not getattr(settings, 'SECURE_SSL_REDIRECT', False):
issues.append('SECURE_SSL_REDIRECT should be True in production')
else:
self.stdout.write(self.style.SUCCESS('✓ SSL redirect enabled'))
# Check password hashers
if 'argon2' in settings.PASSWORD_HASHERS[0].lower():
self.stdout.write(self.style.SUCCESS('✓ Using Argon2 password hasher'))
else:
warnings.append('Consider using Argon2 password hasher')
# Check session security
if settings.SESSION_COOKIE_HTTPONLY:
self.stdout.write(self.style.SUCCESS('✓ Session cookies are HTTP-only'))
else:
issues.append('SESSION_COOKIE_HTTPONLY should be True')
if settings.SESSION_COOKIE_SECURE or settings.DEBUG:
self.stdout.write(self.style.SUCCESS('✓ Session cookies are secure'))
else:
issues.append('SESSION_COOKIE_SECURE should be True in production')
# Check CSRF protection
if settings.CSRF_COOKIE_HTTPONLY:
self.stdout.write(self.style.SUCCESS('✓ CSRF cookies are HTTP-only'))
else:
issues.append('CSRF_COOKIE_HTTPONLY should be True')
# Check failed login attempts
recent_failures = FailedLoginAttempt.objects.filter(
timestamp__gte=timezone.now() - timedelta(hours=24)
).count()
if recent_failures > 0:
self.stdout.write(self.style.WARNING(f'{recent_failures} failed login attempts in last 24 hours'))
else:
self.stdout.write(self.style.SUCCESS('✓ No recent failed login attempts'))
# Check for users with weak passwords (if possible)
users_without_mfa = User.objects.filter(mfa_enabled=False).count()
total_users = User.objects.count()
if total_users > 0:
mfa_percentage = (users_without_mfa / total_users) * 100
if mfa_percentage > 50:
warnings.append(f'Only {100-mfa_percentage:.1f}% of users have MFA enabled')
else:
self.stdout.write(self.style.SUCCESS(f'{100-mfa_percentage:.1f}% of users have MFA enabled'))
# Report issues
if issues:
self.stdout.write(self.style.ERROR('\n' + '=' * 60))
self.stdout.write(self.style.ERROR('CRITICAL ISSUES:'))
for issue in issues:
self.stdout.write(self.style.ERROR(f'{issue}'))
if warnings:
self.stdout.write(self.style.WARNING('\n' + '=' * 60))
self.stdout.write(self.style.WARNING('WARNINGS:'))
for warning in warnings:
self.stdout.write(self.style.WARNING(f'{warning}'))
if not issues and not warnings:
self.stdout.write(self.style.SUCCESS('\n✓ No security issues found!'))
self.stdout.write(self.style.SUCCESS('\n' + '=' * 60))

View File

@@ -0,0 +1,45 @@
"""
Management command to create initial scam tags.
"""
from django.core.management.base import BaseCommand
from reports.models import ScamTag
class Command(BaseCommand):
help = 'Create initial scam tags'
def handle(self, *args, **options):
tags = [
{'name': 'Phishing', 'description': 'Phishing scams', 'color': '#dc3545'},
{'name': 'Fake Website', 'description': 'Fake or fraudulent websites', 'color': '#fd7e14'},
{'name': 'Romance Scam', 'description': 'Romance and dating scams', 'color': '#e83e8c'},
{'name': 'Investment Scam', 'description': 'Investment and financial scams', 'color': '#ffc107'},
{'name': 'Tech Support', 'description': 'Tech support scams', 'color': '#20c997'},
{'name': 'Identity Theft', 'description': 'Identity theft attempts', 'color': '#6f42c1'},
{'name': 'Fake Product', 'description': 'Fake product sales', 'color': '#17a2b8'},
{'name': 'Advance Fee', 'description': 'Advance fee fraud', 'color': '#343a40'},
]
created_count = 0
for tag_data in tags:
tag, created = ScamTag.objects.get_or_create(
name=tag_data['name'],
defaults={
'description': tag_data['description'],
'color': tag_data['color']
}
)
if created:
created_count += 1
self.stdout.write(
self.style.SUCCESS(f'Created tag: {tag.name}')
)
else:
self.stdout.write(
self.style.WARNING(f'Tag already exists: {tag.name}')
)
self.stdout.write(
self.style.SUCCESS(f'\nCreated {created_count} new tags.')
)

View File

@@ -0,0 +1,377 @@
"""
Management command to create sample data for testing.
"""
from django.core.management.base import BaseCommand
from django.contrib.auth import get_user_model
from accounts.models import UserProfile, ActivityLog
from reports.models import ScamReport, ScamTag, ScamVerification
from osint.models import OSINTTask, OSINTResult
from moderation.models import ModerationQueue, ModerationAction
from analytics.models import ReportStatistic, UserStatistic
from legal.models import ConsentRecord
from django.utils import timezone
from datetime import timedelta
import random
User = get_user_model()
class Command(BaseCommand):
help = 'Create sample data for testing'
def add_arguments(self, parser):
parser.add_argument(
'--clear',
action='store_true',
help='Clear existing data before creating sample data',
)
def handle(self, *args, **options):
if options['clear']:
self.stdout.write(self.style.WARNING('Clearing existing data...'))
ScamReport.objects.all().delete()
User.objects.filter(is_superuser=False).delete()
ScamTag.objects.all().delete()
self.stdout.write(self.style.SUCCESS('Creating sample data...'))
# Create users
users = self.create_users()
# Create tags
tags = self.create_tags()
# Create reports
reports = self.create_reports(users, tags)
# Create OSINT data
self.create_osint_data(reports, users)
# Create moderation data
self.create_moderation_data(reports, users)
# Create analytics data
self.create_analytics_data()
self.stdout.write(self.style.SUCCESS('\nSample data created successfully!'))
self.stdout.write(self.style.SUCCESS(f'Created {len(users)} users'))
self.stdout.write(self.style.SUCCESS(f'Created {len(tags)} tags'))
self.stdout.write(self.style.SUCCESS(f'Created {len(reports)} reports'))
def create_users(self):
"""Create sample users."""
users = []
# Create admin user
admin, created = User.objects.get_or_create(
username='admin',
defaults={
'email': 'admin@fraudplatform.bg',
'role': 'admin',
'is_verified': True,
'is_staff': True,
}
)
if created:
admin.set_password('admin123')
admin.save()
UserProfile.objects.create(
user=admin,
first_name='Admin',
last_name='User',
consent_given=True,
consent_date=timezone.now()
)
self.stdout.write(self.style.SUCCESS(f'Created admin user: {admin.username}'))
users.append(admin)
# Create moderator users
for i in range(2):
mod, created = User.objects.get_or_create(
username=f'moderator{i+1}',
defaults={
'email': f'moderator{i+1}@fraudplatform.bg',
'role': 'moderator',
'is_verified': True,
}
)
if created:
mod.set_password('mod123')
mod.save()
UserProfile.objects.create(
user=mod,
first_name=f'Moderator{i+1}',
last_name='User',
consent_given=True,
consent_date=timezone.now()
)
self.stdout.write(self.style.SUCCESS(f'Created moderator: {mod.username}'))
users.append(mod)
# Create normal users
user_data = [
('john_doe', 'john@example.com', 'John', 'Doe'),
('jane_smith', 'jane@example.com', 'Jane', 'Smith'),
('ivan_petrov', 'ivan@example.com', 'Ivan', 'Petrov'),
('maria_georgieva', 'maria@example.com', 'Maria', 'Georgieva'),
('test_user', 'test@example.com', 'Test', 'User'),
]
for username, email, first_name, last_name in user_data:
user, created = User.objects.get_or_create(
username=username,
defaults={
'email': email,
'role': 'normal',
'is_verified': True,
}
)
if created:
user.set_password('user123')
user.save()
UserProfile.objects.create(
user=user,
first_name=first_name,
last_name=last_name,
consent_given=True,
consent_date=timezone.now()
)
self.stdout.write(self.style.SUCCESS(f'Created user: {user.username}'))
users.append(user)
return users
def create_tags(self):
"""Create sample tags."""
tag_data = [
('Phishing', 'Phishing scams', '#dc3545'),
('Fake Website', 'Fake or fraudulent websites', '#fd7e14'),
('Romance Scam', 'Romance and dating scams', '#e83e8c'),
('Investment Scam', 'Investment and financial scams', '#ffc107'),
('Tech Support', 'Tech support scams', '#20c997'),
('Identity Theft', 'Identity theft attempts', '#6f42c1'),
('Fake Product', 'Fake product sales', '#17a2b8'),
('Advance Fee', 'Advance fee fraud', '#343a40'),
]
tags = []
for name, description, color in tag_data:
tag, created = ScamTag.objects.get_or_create(
name=name,
defaults={
'description': description,
'color': color
}
)
if created:
self.stdout.write(self.style.SUCCESS(f'Created tag: {tag.name}'))
tags.append(tag)
return tags
def create_reports(self, users, tags):
"""Create sample scam reports."""
normal_users = [u for u in users if u.role == 'normal']
if not normal_users:
return []
report_data = [
{
'title': 'Fake Bulgarian Bank Website',
'description': 'I received an email claiming to be from my bank asking me to verify my account. The website looked identical to the real bank website but the URL was slightly different. When I entered my credentials, I realized it was a phishing attempt.',
'scam_type': 'phishing',
'reported_url': 'https://fake-bank-bg.com',
'reported_email': 'support@fake-bank-bg.com',
'status': 'verified',
'verification_score': 95,
},
{
'title': 'Romance Scam on Dating Site',
'description': 'Someone contacted me on a dating site and after weeks of chatting, asked me to send money for an emergency. I later found out this was a common romance scam pattern.',
'scam_type': 'romance_scam',
'reported_email': 'scammer@example.com',
'reported_phone': '+359888123456',
'status': 'verified',
'verification_score': 88,
},
{
'title': 'Fake Investment Opportunity',
'description': 'Received a call about a "guaranteed" investment opportunity with high returns. They asked for an upfront fee and promised unrealistic returns. This is clearly a scam.',
'scam_type': 'investment_scam',
'reported_phone': '+359888654321',
'reported_company': 'Fake Investment Group',
'status': 'verified',
'verification_score': 92,
},
{
'title': 'Tech Support Scam Call',
'description': 'Received a call from someone claiming to be from Microsoft tech support. They said my computer was infected and asked me to install remote access software. This is a known tech support scam.',
'scam_type': 'tech_support_scam',
'reported_phone': '+359888999888',
'status': 'verified',
'verification_score': 90,
},
{
'title': 'Fake Online Store',
'description': 'Ordered a product from an online store that looked legitimate. After payment, I never received the product and the website disappeared. The products were fake listings.',
'scam_type': 'fake_product',
'reported_url': 'https://fake-store-bg.com',
'reported_email': 'orders@fake-store-bg.com',
'status': 'verified',
'verification_score': 85,
},
{
'title': 'Phishing Email - Tax Refund',
'description': 'Received an email claiming I was eligible for a tax refund. The email asked me to click a link and provide personal information. This is a phishing attempt.',
'scam_type': 'phishing',
'reported_email': 'tax-refund@scam.com',
'status': 'pending',
'verification_score': 0,
},
{
'title': 'Advance Fee Fraud - Lottery Win',
'description': 'Received an email claiming I won a lottery I never entered. They asked for payment of "processing fees" to claim the prize. This is advance fee fraud.',
'scam_type': 'advance_fee',
'reported_email': 'lottery@scam.com',
'status': 'under_review',
'verification_score': 75,
},
{
'title': 'Fake Job Offer',
'description': 'Received a job offer via email that seemed too good to be true. They asked for personal documents and bank account information before any interview. This is a scam.',
'scam_type': 'other',
'reported_email': 'hr@fake-company.com',
'reported_url': 'https://fake-jobs-bg.com',
'status': 'verified',
'verification_score': 87,
},
]
reports = []
for i, data in enumerate(report_data):
reporter = random.choice(normal_users)
created_at = timezone.now() - timedelta(days=random.randint(1, 30))
report = ScamReport.objects.create(
reporter=reporter,
title=data['title'],
description=data['description'],
scam_type=data['scam_type'],
reported_url=data.get('reported_url', ''),
reported_email=data.get('reported_email', ''),
reported_phone=data.get('reported_phone', ''),
reported_company=data.get('reported_company', ''),
status=data['status'],
verification_score=data['verification_score'],
is_public=True if data['status'] == 'verified' else False,
is_anonymous=random.choice([True, False]),
created_at=created_at,
)
# Add random tags
report.tags.set(random.sample(tags, random.randint(1, 3)))
if data['status'] == 'verified':
report.verified_at = created_at + timedelta(hours=random.randint(1, 48))
report.save()
reports.append(report)
self.stdout.write(self.style.SUCCESS(f'Created report: {report.title}'))
return reports
def create_osint_data(self, reports, users):
"""Create sample OSINT data."""
moderators = [u for u in users if u.role in ['moderator', 'admin']]
if not moderators or not reports:
return
for report in reports[:5]: # Add OSINT data to first 5 reports
# Create OSINT tasks
task_types = ['whois_lookup', 'dns_lookup', 'ssl_check', 'email_analysis']
for task_type in random.sample(task_types, 2):
OSINTTask.objects.create(
report=report,
task_type=task_type,
status='completed',
parameters={'target': report.reported_url or report.reported_email or report.reported_phone},
result={'status': 'success', 'data': 'Sample OSINT data'},
started_at=report.created_at + timedelta(minutes=5),
completed_at=report.created_at + timedelta(minutes=10),
)
# Create OSINT results
OSINTResult.objects.create(
report=report,
source='WHOIS Lookup',
data_type='whois',
raw_data={'domain': report.reported_url, 'registrar': 'Fake Registrar'},
processed_data={'risk_level': 'high', 'domain_age': '30 days'},
confidence_level=85,
is_verified=True,
)
self.stdout.write(self.style.SUCCESS(f'Created OSINT data for: {report.title}'))
def create_moderation_data(self, reports, users):
"""Create sample moderation data."""
moderators = [u for u in users if u.role in ['moderator', 'admin']]
if not moderators or not reports:
return
# Add pending reports to moderation queue
pending_reports = [r for r in reports if r.status == 'pending']
for report in pending_reports:
ModerationQueue.objects.create(
report=report,
priority=random.choice(['low', 'normal', 'high']),
assigned_to=random.choice(moderators) if random.choice([True, False]) else None,
)
# Create moderation actions for verified reports
verified_reports = [r for r in reports if r.status == 'verified']
for report in verified_reports:
moderator = random.choice(moderators)
ModerationAction.objects.create(
report=report,
moderator=moderator,
action_type='approve',
previous_status='pending',
new_status='verified',
reason='Verified through OSINT and manual review',
created_at=report.verified_at or report.created_at + timedelta(hours=1),
)
self.stdout.write(self.style.SUCCESS(f'Created moderation data for {len(reports)} reports'))
def create_analytics_data(self):
"""Create sample analytics data."""
today = timezone.now().date()
# Create report statistics for last 7 days
for i in range(7):
date = today - timedelta(days=i)
ReportStatistic.objects.get_or_create(
date=date,
defaults={
'total_reports': ScamReport.objects.filter(created_at__date=date).count(),
'pending_reports': ScamReport.objects.filter(status='pending', created_at__date=date).count(),
'verified_reports': ScamReport.objects.filter(status='verified', created_at__date=date).count(),
'rejected_reports': ScamReport.objects.filter(status='rejected', created_at__date=date).count(),
}
)
# Create user statistics
UserStatistic.objects.get_or_create(
date=today,
defaults={
'total_users': User.objects.count(),
'new_users': User.objects.filter(created_at__date=today).count(),
'active_users': User.objects.filter(last_login__date=today).count(),
'moderators': User.objects.filter(role__in=['moderator', 'admin']).count(),
'admins': User.objects.filter(role='admin').count(),
}
)
self.stdout.write(self.style.SUCCESS('Created analytics data'))

View File

@@ -0,0 +1,120 @@
"""
Management command to create test users for dashboard testing.
"""
from django.core.management.base import BaseCommand
from django.contrib.auth import get_user_model
from accounts.models import UserProfile
from django.utils import timezone
User = get_user_model()
class Command(BaseCommand):
help = 'Create test users (normal, moderator, admin) for dashboard testing'
def handle(self, *args, **options):
self.stdout.write(self.style.SUCCESS('Creating test users...'))
# Create/Update Normal User
normal_user, created = User.objects.get_or_create(
username='normal_user',
defaults={
'email': 'normal@test.bg',
'role': 'normal',
'is_verified': True,
}
)
normal_user.set_password('normal123')
normal_user.role = 'normal'
normal_user.is_verified = True
normal_user.save()
if not hasattr(normal_user, 'profile'):
UserProfile.objects.create(
user=normal_user,
first_name='Normal',
last_name='User',
consent_given=True,
consent_date=timezone.now()
)
self.stdout.write(self.style.SUCCESS(
f'{"Created" if created else "Updated"} normal user: {normal_user.username} (password: normal123)'
))
# Create/Update Moderator User
moderator_user, created = User.objects.get_or_create(
username='moderator',
defaults={
'email': 'moderator@test.bg',
'role': 'moderator',
'is_verified': True,
}
)
moderator_user.set_password('moderator123')
moderator_user.role = 'moderator'
moderator_user.is_verified = True
moderator_user.save()
if not hasattr(moderator_user, 'profile'):
UserProfile.objects.create(
user=moderator_user,
first_name='Moderator',
last_name='User',
consent_given=True,
consent_date=timezone.now()
)
self.stdout.write(self.style.SUCCESS(
f'{"Created" if created else "Updated"} moderator user: {moderator_user.username} (password: moderator123)'
))
# Create/Update Admin User
admin_user, created = User.objects.get_or_create(
username='admin',
defaults={
'email': 'admin@test.bg',
'role': 'admin',
'is_verified': True,
'is_staff': True,
'is_superuser': True,
}
)
admin_user.set_password('admin123')
admin_user.role = 'admin'
admin_user.is_verified = True
admin_user.is_staff = True
admin_user.is_superuser = True
admin_user.save()
if not hasattr(admin_user, 'profile'):
UserProfile.objects.create(
user=admin_user,
first_name='Admin',
last_name='User',
consent_given=True,
consent_date=timezone.now()
)
self.stdout.write(self.style.SUCCESS(
f'{"Created" if created else "Updated"} admin user: {admin_user.username} (password: admin123)'
))
self.stdout.write(self.style.SUCCESS('\n' + '='*60))
self.stdout.write(self.style.SUCCESS('Test Users Created Successfully!'))
self.stdout.write(self.style.SUCCESS('='*60))
self.stdout.write(self.style.SUCCESS('\nLogin Credentials:'))
self.stdout.write(self.style.SUCCESS('\n1. Normal User:'))
self.stdout.write(self.style.SUCCESS(' Username: normal_user'))
self.stdout.write(self.style.SUCCESS(' Password: normal123'))
self.stdout.write(self.style.SUCCESS(' Dashboard: /reports/my-reports/'))
self.stdout.write(self.style.SUCCESS('\n2. Moderator:'))
self.stdout.write(self.style.SUCCESS(' Username: moderator'))
self.stdout.write(self.style.SUCCESS(' Password: moderator123'))
self.stdout.write(self.style.SUCCESS(' Dashboard: /moderation/dashboard/'))
self.stdout.write(self.style.SUCCESS('\n3. Administrator:'))
self.stdout.write(self.style.SUCCESS(' Username: admin'))
self.stdout.write(self.style.SUCCESS(' Password: admin123'))
self.stdout.write(self.style.SUCCESS(' Dashboard: /analytics/dashboard/'))
self.stdout.write(self.style.SUCCESS('\n' + '='*60))

279
accounts/middleware.py Normal file
View File

@@ -0,0 +1,279 @@
"""
Security middleware for enhanced protection.
"""
import time
from django.core.cache import cache
from django.http import HttpResponseForbidden, JsonResponse
from django.utils.deprecation import MiddlewareMixin
from django.contrib.auth import logout
from django.contrib import messages
from accounts.models import FailedLoginAttempt, ActivityLog
from django.utils import timezone
from datetime import timedelta, datetime
class RateLimitMiddleware(MiddlewareMixin):
"""
Rate limiting middleware to prevent brute force attacks.
"""
def process_request(self, request):
# Skip rate limiting for static files and admin
if request.path.startswith('/static/') or request.path.startswith('/media/'):
return None
# Get client IP
ip = self.get_client_ip(request)
# Rate limit login attempts
if request.path == '/accounts/login/' and request.method == 'POST':
cache_key = f'login_attempts_{ip}'
attempts = cache.get(cache_key, 0)
if attempts >= 5: # Max 5 attempts per 15 minutes
# Log failed attempt
FailedLoginAttempt.objects.create(
email_or_username=request.POST.get('username', ''),
ip_address=ip,
user_agent=request.META.get('HTTP_USER_AGENT', ''),
is_blocked=True
)
return JsonResponse({
'error': 'Too many login attempts. Please try again in 15 minutes.'
}, status=429)
# Increment attempts
cache.set(cache_key, attempts + 1, 900) # 15 minutes
# Rate limit registration
if request.path == '/accounts/register/' and request.method == 'POST':
cache_key = f'register_attempts_{ip}'
attempts = cache.get(cache_key, 0)
if attempts >= 3: # Max 3 registrations per hour
return JsonResponse({
'error': 'Too many registration attempts. Please try again later.'
}, status=429)
cache.set(cache_key, attempts + 1, 3600) # 1 hour
# Rate limit report creation
if request.path.startswith('/reports/create/') and request.method == 'POST':
if request.user.is_authenticated:
cache_key = f'report_creation_{request.user.id}'
attempts = cache.get(cache_key, 0)
if attempts >= 10: # Max 10 reports per hour
return JsonResponse({
'error': 'Too many reports created. Please try again later.'
}, status=429)
cache.set(cache_key, attempts + 1, 3600) # 1 hour
return None
def get_client_ip(self, request):
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('REMOTE_ADDR')
return ip
class SecurityHeadersMiddleware(MiddlewareMixin):
"""
Add security headers to all responses.
"""
def process_response(self, request, response):
# Content Security Policy
csp = (
"default-src 'self'; "
"script-src 'self' 'unsafe-inline' https://fonts.googleapis.com; "
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; "
"font-src 'self' https://fonts.gstatic.com; "
"img-src 'self' data: https:; "
"connect-src 'self'; "
"frame-ancestors 'none'; "
"base-uri 'self'; "
"form-action 'self';"
)
response['Content-Security-Policy'] = csp
# X-Content-Type-Options
response['X-Content-Type-Options'] = 'nosniff'
# X-Frame-Options
response['X-Frame-Options'] = 'DENY'
# Referrer Policy
response['Referrer-Policy'] = 'strict-origin-when-cross-origin'
# Permissions Policy
response['Permissions-Policy'] = (
'geolocation=(), microphone=(), camera=(), '
'payment=(), usb=(), magnetometer=(), gyroscope=()'
)
# X-XSS-Protection (legacy but still useful)
response['X-XSS-Protection'] = '1; mode=block'
# Remove server header
if 'Server' in response:
del response['Server']
return response
class IPWhitelistMiddleware(MiddlewareMixin):
"""
IP whitelist/blacklist middleware (optional, for admin access).
"""
def process_request(self, request):
# Only apply to admin area
if not request.path.startswith('/admin/'):
return None
ip = self.get_client_ip(request)
# Get blacklisted IPs from cache or database
blacklisted = cache.get(f'blacklisted_ip_{ip}', False)
if blacklisted:
return HttpResponseForbidden('Access denied.')
return None
def get_client_ip(self, request):
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('REMOTE_ADDR')
return ip
class SecurityLoggingMiddleware(MiddlewareMixin):
"""
Log security-related events.
"""
def process_response(self, request, response):
# Log suspicious activities
if response.status_code == 403:
self.log_security_event(request, 'FORBIDDEN_ACCESS', {
'path': request.path,
'method': request.method,
'status': 403
})
if response.status_code == 429:
self.log_security_event(request, 'RATE_LIMIT_EXCEEDED', {
'path': request.path,
'method': request.method,
})
# Log failed login attempts
if request.path == '/accounts/login/' and request.method == 'POST':
if response.status_code != 200 or (hasattr(response, 'content') and b'error' in response.content):
self.log_security_event(request, 'FAILED_LOGIN', {
'username': request.POST.get('username', ''),
})
return response
def log_security_event(self, request, event_type, details):
"""Log security event to ActivityLog."""
try:
ip = self.get_client_ip(request)
# Convert non-JSON-serializable objects to strings
serializable_details = self.make_json_serializable({
'event_type': event_type,
**details
})
ActivityLog.objects.create(
user=request.user if hasattr(request, 'user') and request.user.is_authenticated else None,
action='security_event',
ip_address=ip,
user_agent=request.META.get('HTTP_USER_AGENT', ''),
details=serializable_details
)
except Exception:
pass # Don't break the request if logging fails
def make_json_serializable(self, obj):
"""Convert non-JSON-serializable objects to strings."""
import json
from datetime import datetime, date
from decimal import Decimal
if isinstance(obj, dict):
return {k: self.make_json_serializable(v) for k, v in obj.items()}
elif isinstance(obj, (list, tuple)):
return [self.make_json_serializable(item) for item in obj]
elif isinstance(obj, (datetime, date)):
return obj.isoformat()
elif isinstance(obj, Decimal):
return float(obj)
elif hasattr(obj, '__dict__'):
return str(obj)
else:
try:
json.dumps(obj) # Test if it's JSON serializable
return obj
except (TypeError, ValueError):
return str(obj)
def get_client_ip(self, request):
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('REMOTE_ADDR')
return ip
class SessionSecurityMiddleware(MiddlewareMixin):
"""
Enhanced session security.
"""
def process_request(self, request):
# Check if user attribute exists (AuthenticationMiddleware must run first)
if hasattr(request, 'user') and request.user.is_authenticated:
# Check for session hijacking
current_ip = self.get_client_ip(request)
session_ip = request.session.get('ip_address')
if session_ip and session_ip != current_ip:
# IP changed - potential session hijacking
logout(request)
messages.error(request, 'Security alert: Session terminated due to IP change.')
return None
# Store IP in session
request.session['ip_address'] = current_ip
# Check session age
session_age = request.session.get('created_at')
if session_age:
# Convert string back to datetime if needed
if isinstance(session_age, str):
from django.utils.dateparse import parse_datetime
session_age = parse_datetime(session_age)
if isinstance(session_age, datetime):
age = timezone.now() - session_age
if age > timedelta(hours=24): # Max 24 hours
logout(request)
messages.info(request, 'Your session has expired. Please log in again.')
return None
else:
request.session['created_at'] = timezone.now().isoformat()
return None
def get_client_ip(self, request):
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('REMOTE_ADDR')
return ip

View File

@@ -0,0 +1,114 @@
# Generated by Django 5.2.8 on 2025-11-26 13:41
import django.contrib.auth.models
import django.contrib.auth.validators
import django.core.validators
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.CreateModel(
name='User',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('role', models.CharField(choices=[('normal', 'Normal User'), ('moderator', 'Moderator'), ('admin', 'Administrator')], default='normal', help_text='User role in the system', max_length=20)),
('is_verified', models.BooleanField(default=False, help_text='Email verification status')),
('mfa_enabled', models.BooleanField(default=False, help_text='Multi-factor authentication enabled')),
('mfa_secret', models.CharField(blank=True, help_text='MFA secret key', max_length=32, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('last_login_ip', models.GenericIPAddressField(blank=True, null=True)),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
],
options={
'verbose_name': 'User',
'verbose_name_plural': 'Users',
'db_table': 'users_user',
'ordering': ['-created_at'],
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
migrations.CreateModel(
name='FailedLoginAttempt',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('email_or_username', models.CharField(max_length=255)),
('ip_address', models.GenericIPAddressField()),
('user_agent', models.TextField(blank=True)),
('timestamp', models.DateTimeField(auto_now_add=True)),
('is_blocked', models.BooleanField(default=False)),
],
options={
'verbose_name': 'Failed Login Attempt',
'verbose_name_plural': 'Failed Login Attempts',
'db_table': 'security_failedlogin',
'ordering': ['-timestamp'],
'indexes': [models.Index(fields=['email_or_username', 'timestamp'], name='security_fa_email_o_e830a6_idx'), models.Index(fields=['ip_address', 'timestamp'], name='security_fa_ip_addr_d5cb75_idx')],
},
),
migrations.CreateModel(
name='UserProfile',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('first_name', models.CharField(blank=True, max_length=100)),
('last_name', models.CharField(blank=True, max_length=100)),
('phone', models.CharField(blank=True, help_text='Encrypted phone number', max_length=17, null=True, validators=[django.core.validators.RegexValidator(message="Phone number must be entered in the format: '+999999999'. Up to 15 digits allowed.", regex='^\\+?1?\\d{9,15}$')])),
('date_of_birth', models.DateField(blank=True, null=True)),
('consent_given', models.BooleanField(default=False)),
('consent_date', models.DateTimeField(blank=True, null=True)),
('consent_ip', models.GenericIPAddressField(blank=True, null=True)),
('preferred_language', models.CharField(choices=[('bg', 'Bulgarian'), ('en', 'English')], default='bg', max_length=10)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'User Profile',
'verbose_name_plural': 'User Profiles',
'db_table': 'users_userprofile',
},
),
migrations.CreateModel(
name='ActivityLog',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('action', models.CharField(choices=[('login', 'Login'), ('logout', 'Logout'), ('register', 'Registration'), ('password_change', 'Password Change'), ('profile_update', 'Profile Update'), ('report_create', 'Report Created'), ('report_edit', 'Report Edited'), ('report_delete', 'Report Deleted')], max_length=50)),
('ip_address', models.GenericIPAddressField(blank=True, null=True)),
('user_agent', models.TextField(blank=True)),
('details', models.JSONField(blank=True, default=dict)),
('timestamp', models.DateTimeField(auto_now_add=True)),
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='activity_logs', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Activity Log',
'verbose_name_plural': 'Activity Logs',
'db_table': 'users_activitylog',
'ordering': ['-timestamp'],
'indexes': [models.Index(fields=['user', 'timestamp'], name='users_activ_user_id_049bc2_idx'), models.Index(fields=['action', 'timestamp'], name='users_activ_action_cdfe71_idx')],
},
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2.8 on 2025-11-26 14:40
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='activitylog',
name='action',
field=models.CharField(choices=[('login', 'Login'), ('logout', 'Logout'), ('register', 'Registration'), ('password_change', 'Password Change'), ('profile_update', 'Profile Update'), ('report_create', 'Report Created'), ('report_edit', 'Report Edited'), ('report_delete', 'Report Deleted'), ('security_event', 'Security Event'), ('failed_login', 'Failed Login'), ('suspicious_activity', 'Suspicious Activity')], max_length=50),
),
]

View File

175
accounts/models.py Normal file
View File

@@ -0,0 +1,175 @@
"""
User management models for the fraud reporting platform.
"""
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.utils import timezone
from django.core.validators import RegexValidator
class User(AbstractUser):
"""
Custom user model with role-based access control.
"""
ROLE_CHOICES = [
('normal', 'Normal User'),
('moderator', 'Moderator'),
('admin', 'Administrator'),
]
role = models.CharField(
max_length=20,
choices=ROLE_CHOICES,
default='normal',
help_text='User role in the system'
)
is_verified = models.BooleanField(
default=False,
help_text='Email verification status'
)
mfa_enabled = models.BooleanField(
default=False,
help_text='Multi-factor authentication enabled'
)
mfa_secret = models.CharField(
max_length=32,
blank=True,
null=True,
help_text='MFA secret key'
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
last_login_ip = models.GenericIPAddressField(null=True, blank=True)
class Meta:
db_table = 'users_user'
verbose_name = 'User'
verbose_name_plural = 'Users'
ordering = ['-created_at']
def __str__(self):
return f"{self.username} ({self.role})"
def is_moderator(self):
return self.role in ['moderator', 'admin']
def is_administrator(self):
return self.role == 'admin'
class UserProfile(models.Model):
"""
Extended user profile information.
"""
user = models.OneToOneField(
User,
on_delete=models.CASCADE,
related_name='profile'
)
first_name = models.CharField(max_length=100, blank=True)
last_name = models.CharField(max_length=100, blank=True)
phone_regex = RegexValidator(
regex=r'^\+?1?\d{9,15}$',
message="Phone number must be entered in the format: '+999999999'. Up to 15 digits allowed."
)
phone = models.CharField(
validators=[phone_regex],
max_length=17,
blank=True,
null=True,
help_text='Encrypted phone number'
)
date_of_birth = models.DateField(null=True, blank=True)
# GDPR Consent
consent_given = models.BooleanField(default=False)
consent_date = models.DateTimeField(null=True, blank=True)
consent_ip = models.GenericIPAddressField(null=True, blank=True)
# Preferences
preferred_language = models.CharField(
max_length=10,
default='bg',
choices=[('bg', 'Bulgarian'), ('en', 'English')]
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'users_userprofile'
verbose_name = 'User Profile'
verbose_name_plural = 'User Profiles'
def __str__(self):
return f"Profile of {self.user.username}"
class ActivityLog(models.Model):
"""
Log user activities for security and auditing.
"""
ACTION_CHOICES = [
('login', 'Login'),
('logout', 'Logout'),
('register', 'Registration'),
('password_change', 'Password Change'),
('profile_update', 'Profile Update'),
('report_create', 'Report Created'),
('report_edit', 'Report Edited'),
('report_delete', 'Report Deleted'),
('security_event', 'Security Event'),
('failed_login', 'Failed Login'),
('suspicious_activity', 'Suspicious Activity'),
]
user = models.ForeignKey(
User,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='activity_logs'
)
action = models.CharField(max_length=50, choices=ACTION_CHOICES)
ip_address = models.GenericIPAddressField(null=True, blank=True)
user_agent = models.TextField(blank=True)
details = models.JSONField(default=dict, blank=True)
timestamp = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = 'users_activitylog'
verbose_name = 'Activity Log'
verbose_name_plural = 'Activity Logs'
ordering = ['-timestamp']
indexes = [
models.Index(fields=['user', 'timestamp']),
models.Index(fields=['action', 'timestamp']),
]
def __str__(self):
return f"{self.user} - {self.action} at {self.timestamp}"
class FailedLoginAttempt(models.Model):
"""
Track failed login attempts for security.
"""
email_or_username = models.CharField(max_length=255)
ip_address = models.GenericIPAddressField()
user_agent = models.TextField(blank=True)
timestamp = models.DateTimeField(auto_now_add=True)
is_blocked = models.BooleanField(default=False)
class Meta:
db_table = 'security_failedlogin'
verbose_name = 'Failed Login Attempt'
verbose_name_plural = 'Failed Login Attempts'
ordering = ['-timestamp']
indexes = [
models.Index(fields=['email_or_username', 'timestamp']),
models.Index(fields=['ip_address', 'timestamp']),
]
def __str__(self):
return f"Failed login: {self.email_or_username} from {self.ip_address}"

134
accounts/security.py Normal file
View File

@@ -0,0 +1,134 @@
"""
Security utilities for encryption and data protection.
"""
from cryptography.fernet import Fernet
from django.conf import settings
import base64
import hashlib
import os
class DataEncryption:
"""
Encrypt/decrypt sensitive data.
"""
@staticmethod
def get_encryption_key():
"""Get or generate encryption key."""
key = getattr(settings, 'ENCRYPTION_KEY', None)
if not key:
# Generate a key (in production, this should be in environment)
key = Fernet.generate_key()
elif isinstance(key, str):
key = key.encode()
return key
@staticmethod
def encrypt(data):
"""Encrypt sensitive data."""
if not data:
return data
try:
key = DataEncryption.get_encryption_key()
f = Fernet(key)
encrypted = f.encrypt(data.encode() if isinstance(data, str) else data)
return base64.urlsafe_b64encode(encrypted).decode()
except Exception:
return data # Return original if encryption fails
@staticmethod
def decrypt(encrypted_data):
"""Decrypt sensitive data."""
if not encrypted_data:
return encrypted_data
try:
key = DataEncryption.get_encryption_key()
f = Fernet(key)
decoded = base64.urlsafe_b64decode(encrypted_data.encode())
decrypted = f.decrypt(decoded)
return decrypted.decode()
except Exception:
return encrypted_data # Return original if decryption fails
class InputSanitizer:
"""
Sanitize user input to prevent XSS and injection attacks.
"""
@staticmethod
def sanitize_html(text):
"""Remove potentially dangerous HTML."""
if not text:
return text
import html
# Escape HTML entities
text = html.escape(text)
return text
@staticmethod
def sanitize_sql(text):
"""Basic SQL injection prevention (Django ORM handles this, but extra check)."""
if not text:
return text
# Remove SQL keywords
dangerous = ['DROP', 'DELETE', 'INSERT', 'UPDATE', 'SELECT', 'UNION', '--', ';']
text_upper = text.upper()
for keyword in dangerous:
if keyword in text_upper:
# Log potential SQL injection attempt
return None
return text
@staticmethod
def validate_email(email):
"""Validate email format."""
import re
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
return bool(re.match(pattern, email))
@staticmethod
def validate_url(url):
"""Validate URL format."""
import re
pattern = r'^https?://[^\s/$.?#].[^\s]*$'
return bool(re.match(pattern, url))
class PasswordSecurity:
"""
Enhanced password security utilities.
"""
@staticmethod
def check_password_strength(password):
"""Check password strength."""
if len(password) < 12:
return False, "Password must be at least 12 characters long"
if not any(c.isupper() for c in password):
return False, "Password must contain at least one uppercase letter"
if not any(c.islower() for c in password):
return False, "Password must contain at least one lowercase letter"
if not any(c.isdigit() for c in password):
return False, "Password must contain at least one number"
if not any(c in "!@#$%^&*()_+-=[]{}|;:,.<>?" for c in password):
return False, "Password must contain at least one special character"
# Check for common patterns
common_patterns = ['123456', 'password', 'qwerty', 'abc123']
password_lower = password.lower()
for pattern in common_patterns:
if pattern in password_lower:
return False, "Password contains common patterns"
return True, "Password is strong"
@staticmethod
def hash_sensitive_data(data):
"""Hash sensitive data for storage."""
return hashlib.sha256(data.encode()).hexdigest()

3
accounts/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

33
accounts/urls.py Normal file
View File

@@ -0,0 +1,33 @@
"""
URL configuration for accounts app.
"""
from django.urls import path
from django.contrib.auth import views as auth_views
from . import views
app_name = 'accounts'
urlpatterns = [
# Authentication
path('login/', views.LoginView.as_view(), name='login'),
path('logout/', views.LogoutView.as_view(), name='logout'),
path('register/', views.RegisterView.as_view(), name='register'),
# MFA
path('mfa/verify/', views.MFAVerifyView.as_view(), name='mfa_verify'),
path('mfa/setup/', views.MFASetupView.as_view(), name='mfa_setup'),
path('mfa/enable/', views.MFAEnableView.as_view(), name='mfa_enable'),
path('mfa/disable/', views.MFADisableView.as_view(), name='mfa_disable'),
# Profile
path('profile/', views.ProfileView.as_view(), name='profile'),
path('profile/edit/', views.ProfileEditView.as_view(), name='profile_edit'),
# Password management
path('password/change/', views.PasswordChangeView.as_view(), name='password_change'),
path('password/reset/', views.PasswordResetView.as_view(), name='password_reset'),
path('password/reset/done/', views.PasswordResetDoneView.as_view(), name='password_reset_done'),
path('password/reset/confirm/<uidb64>/<token>/', views.PasswordResetConfirmView.as_view(), name='password_reset_confirm'),
path('password/reset/complete/', auth_views.PasswordResetCompleteView.as_view(), name='password_reset_complete'),
]

433
accounts/views.py Normal file
View File

@@ -0,0 +1,433 @@
"""
Views for accounts app.
"""
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth import login, logout
from django.contrib.auth.views import LoginView, PasswordChangeView, PasswordResetView, PasswordResetConfirmView, PasswordResetDoneView
from django.views.generic import CreateView, UpdateView, DetailView, TemplateView, FormView, View
from django.urls import reverse_lazy
from django.contrib.messages.views import SuccessMessageMixin
from django.contrib.messages import success, error
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from django_otp import devices_for_user
from django_otp.plugins.otp_totp.models import TOTPDevice
from django_otp.decorators import otp_required
from django.views.decorators.csrf import csrf_exempt
from django.http import JsonResponse
import qrcode
import qrcode.image.svg
from io import BytesIO
import base64
from .models import User, UserProfile, ActivityLog, FailedLoginAttempt
from .forms import UserRegistrationForm, UserProfileForm, MFAVerifyForm, MFASetupForm
from .security import InputSanitizer, PasswordSecurity
from django.core.cache import cache
from django.utils import timezone
from datetime import timedelta
class RegisterView(SuccessMessageMixin, CreateView):
"""User registration view with security checks."""
model = User
form_class = UserRegistrationForm
template_name = 'accounts/register.html'
success_url = reverse_lazy('accounts:profile')
success_message = "Registration successful! Please verify your email."
def get_form_kwargs(self):
"""Pass request to form for rate limiting."""
kwargs = super().get_form_kwargs()
kwargs['request'] = self.request
return kwargs
def form_valid(self, form):
# Sanitize input
if form.cleaned_data.get('email'):
form.cleaned_data['email'] = InputSanitizer.sanitize_html(form.cleaned_data['email'])
# Check password strength
password = form.cleaned_data.get('password1')
is_strong, message = PasswordSecurity.check_password_strength(password)
if not is_strong:
form.add_error('password1', message)
return self.form_invalid(form)
response = super().form_valid(form)
login(self.request, self.object)
# Log activity
ActivityLog.objects.create(
user=self.object,
action='register',
ip_address=self.get_client_ip(),
user_agent=self.request.META.get('HTTP_USER_AGENT', '')
)
return response
def get_client_ip(self):
x_forwarded_for = self.request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = self.request.META.get('REMOTE_ADDR')
return ip
def csrf_failure(request, reason=""):
"""Custom CSRF failure view."""
from django.contrib import messages
messages.error(request, 'Security error: Invalid request. Please try again.')
return redirect('accounts:login')
class LoginView(SuccessMessageMixin, LoginView):
"""Custom login view with MFA support and security checks."""
template_name = 'accounts/login.html'
redirect_authenticated_user = True
success_message = "Welcome back!"
def form_valid(self, form):
# Check for account lockout
username = form.cleaned_data.get('username')
ip = self.get_client_ip()
# Check if IP is blocked
if self.is_ip_blocked(ip):
error(self.request, 'Too many failed login attempts. Please try again later.')
return redirect('accounts:login')
# Check if user account is locked
try:
user = User.objects.get(username=username)
if self.is_account_locked(user):
error(self.request, 'Account temporarily locked due to too many failed attempts.')
return redirect('accounts:login')
except User.DoesNotExist:
pass # Don't reveal if user exists
# Authenticate user first
response = super().form_valid(form)
user = self.request.user
# Clear failed login attempts on successful login
FailedLoginAttempt.objects.filter(
email_or_username=username,
ip_address=ip
).delete()
cache.delete(f'login_attempts_{ip}')
# Check if MFA is enabled
if user.mfa_enabled:
# Store user ID in session for MFA verification
self.request.session['mfa_user_id'] = user.id
# Don't log them in yet - redirect to MFA verification
logout(self.request)
return redirect('accounts:mfa_verify')
# Log activity for non-MFA users
ActivityLog.objects.create(
user=user,
action='login',
ip_address=ip,
user_agent=self.request.META.get('HTTP_USER_AGENT', '')
)
return response
def form_invalid(self, form):
# Log failed login attempt
username = form.data.get('username', '')
ip = self.get_client_ip()
FailedLoginAttempt.objects.create(
email_or_username=username,
ip_address=ip,
user_agent=self.request.META.get('HTTP_USER_AGENT', '')
)
# Check if should block IP
failed_attempts = FailedLoginAttempt.objects.filter(
ip_address=ip,
timestamp__gte=timezone.now() - timedelta(minutes=15)
).count()
if failed_attempts >= 10:
cache.set(f'blocked_ip_{ip}', True, 3600) # Block for 1 hour
return super().form_invalid(form)
def is_ip_blocked(self, ip):
"""Check if IP is blocked."""
return cache.get(f'blocked_ip_{ip}', False)
def is_account_locked(self, user):
"""Check if user account is locked."""
failed_attempts = FailedLoginAttempt.objects.filter(
email_or_username=user.username,
timestamp__gte=timezone.now() - timedelta(minutes=30)
).count()
return failed_attempts >= 5
def get_client_ip(self):
x_forwarded_for = self.request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = self.request.META.get('REMOTE_ADDR')
return ip
class LogoutView(View):
"""Custom logout view that accepts GET requests."""
def get(self, request, *args, **kwargs):
if request.user.is_authenticated:
# Log activity
ActivityLog.objects.create(
user=request.user,
action='logout',
ip_address=self.get_client_ip(request),
user_agent=request.META.get('HTTP_USER_AGENT', '')
)
logout(request)
success(request, "You have been logged out successfully.")
return redirect('reports:home')
def post(self, request, *args, **kwargs):
return self.get(request, *args, **kwargs)
def get_client_ip(self, request):
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('REMOTE_ADDR')
return ip
class ProfileView(DetailView):
"""User profile view."""
model = User
template_name = 'accounts/profile.html'
context_object_name = 'user_obj'
def get_object(self):
return self.request.user
class ProfileEditView(SuccessMessageMixin, UpdateView):
"""Edit user profile."""
model = UserProfile
form_class = UserProfileForm
template_name = 'accounts/profile_edit.html'
success_url = reverse_lazy('accounts:profile')
success_message = "Profile updated successfully!"
def get_object(self):
profile, created = UserProfile.objects.get_or_create(user=self.request.user)
return profile
def form_valid(self, form):
response = super().form_valid(form)
# Log activity
ActivityLog.objects.create(
user=self.request.user,
action='profile_update',
ip_address=self.get_client_ip(),
user_agent=self.request.META.get('HTTP_USER_AGENT', '')
)
return response
def get_client_ip(self):
x_forwarded_for = self.request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = self.request.META.get('REMOTE_ADDR')
return ip
class PasswordChangeView(SuccessMessageMixin, PasswordChangeView):
"""Change password view."""
template_name = 'accounts/password_change.html'
success_url = reverse_lazy('accounts:profile')
success_message = "Password changed successfully!"
class PasswordResetView(SuccessMessageMixin, PasswordResetView):
"""Password reset view."""
template_name = 'accounts/password_reset.html'
email_template_name = 'accounts/password_reset_email.html'
subject_template_name = 'accounts/password_reset_email_subject.txt'
success_url = reverse_lazy('accounts:password_reset_done')
success_message = "Password reset email sent!"
def form_valid(self, form):
"""Override to use SiteSettings for from_email."""
try:
from reports.models import SiteSettings
site_settings = SiteSettings.get_settings()
self.from_email = site_settings.default_from_email
except:
pass # Fallback to default
return super().form_valid(form)
def get_email_context_data(self, **kwargs):
"""Override to ensure site_name is set for email template."""
context = super().get_email_context_data(**kwargs)
# Ensure site_name is set (Django uses sites framework, but provide fallback)
if 'site_name' not in context or not context['site_name']:
context['site_name'] = 'Портал за Докладване на Измами'
return context
class PasswordResetDoneView(TemplateView):
"""Password reset done view - shows confirmation that email was sent."""
template_name = 'accounts/password_reset_done.html'
class PasswordResetConfirmView(SuccessMessageMixin, PasswordResetConfirmView):
"""Password reset confirmation view."""
template_name = 'accounts/password_reset_confirm.html'
success_url = reverse_lazy('accounts:password_reset_complete')
success_message = "Password reset successful!"
class MFAVerifyView(FormView):
"""MFA verification view after login."""
template_name = 'accounts/mfa_verify.html'
form_class = MFAVerifyForm
def dispatch(self, request, *args, **kwargs):
# Check if user ID is in session
if 'mfa_user_id' not in request.session:
error(request, 'Please log in first.')
return redirect('accounts:login')
return super().dispatch(request, *args, **kwargs)
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
user_id = self.request.session.get('mfa_user_id')
if user_id:
kwargs['user'] = get_object_or_404(User, id=user_id)
return kwargs
def form_valid(self, form):
user_id = self.request.session.get('mfa_user_id')
user = get_object_or_404(User, id=user_id)
# Verify the token
if form.verify_token():
# Login the user
login(self.request, user)
# Clear the session
del self.request.session['mfa_user_id']
# Log activity
ActivityLog.objects.create(
user=user,
action='login',
ip_address=self.get_client_ip(),
user_agent=self.request.META.get('HTTP_USER_AGENT', '')
)
success(self.request, 'Login successful!')
return redirect('accounts:profile')
else:
error(self.request, 'Invalid verification code. Please try again.')
return self.form_invalid(form)
def get_client_ip(self):
x_forwarded_for = self.request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = self.request.META.get('REMOTE_ADDR')
return ip
@method_decorator(login_required, name='dispatch')
class MFASetupView(TemplateView):
"""MFA setup view."""
template_name = 'accounts/mfa_setup.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
user = self.request.user
# Delete any existing unconfirmed devices
TOTPDevice.objects.filter(user=user, confirmed=False).delete()
# Create new TOTP device
device = TOTPDevice.objects.create(
user=user,
name='default',
confirmed=False
)
# Generate QR code
config_url = device.config_url
qr = qrcode.QRCode(version=1, box_size=10, border=5)
qr.add_data(config_url)
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
buffer = BytesIO()
img.save(buffer, format='PNG')
img_str = base64.b64encode(buffer.getvalue()).decode()
context['qr_code'] = f'data:image/png;base64,{img_str}'
context['secret_key'] = device.key
context['device'] = device
context['mfa_enabled'] = user.mfa_enabled
return context
@method_decorator(login_required, name='dispatch')
class MFAEnableView(FormView):
"""Enable MFA after verification."""
template_name = 'accounts/mfa_enable.html'
form_class = MFAVerifyForm
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['user'] = self.request.user
return kwargs
def form_valid(self, form):
user = self.request.user
# Verify the token
if form.verify_token():
# Enable MFA
user.mfa_enabled = True
user.save()
# Confirm the device
device = TOTPDevice.objects.get(user=user, name='default')
device.confirmed = True
device.save()
success(self.request, 'MFA has been enabled successfully!')
return redirect('accounts:profile')
else:
error(self.request, 'Invalid verification code. Please try again.')
return self.form_invalid(form)
@method_decorator(login_required, name='dispatch')
class MFADisableView(TemplateView):
"""Disable MFA."""
template_name = 'accounts/mfa_disable.html'
def post(self, request, *args, **kwargs):
user = request.user
user.mfa_enabled = False
user.mfa_secret = ''
user.save()
# Delete TOTP devices
TOTPDevice.objects.filter(user=user).delete()
success(request, 'MFA has been disabled.')
return redirect('accounts:profile')
def get(self, request, *args, **kwargs):
return super().get(request, *args, **kwargs)