soundwave/backend/user/two_factor.py
Iulian 51679d1943 Initial commit - SoundWave v1.0
- Full PWA support with offline capabilities
- Comprehensive search across songs, playlists, and channels
- Offline playlist manager with download tracking
- Pre-built frontend for zero-build deployment
- Docker-based deployment with docker compose
- Material-UI dark theme interface
- YouTube audio download and management
- Multi-user authentication support
2025-12-16 23:43:07 +00:00

158 lines
4.6 KiB
Python

"""2FA utility functions"""
import pyotp
import qrcode
import io
import base64
import secrets
from reportlab.lib.pagesizes import letter
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import inch
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
from reportlab.lib import colors
from datetime import datetime
def generate_totp_secret():
"""Generate a new TOTP secret"""
return pyotp.random_base32()
def get_totp_uri(secret, username, issuer='SoundWave'):
"""Generate TOTP URI for QR code"""
totp = pyotp.TOTP(secret)
return totp.provisioning_uri(name=username, issuer_name=issuer)
def generate_qr_code(uri):
"""Generate QR code image as base64 string"""
qr = qrcode.QRCode(
version=1,
error_correction=qrcode.constants.ERROR_CORRECT_L,
box_size=10,
border=4,
)
qr.add_data(uri)
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
# Convert to base64
buffer = io.BytesIO()
img.save(buffer, format='PNG')
buffer.seek(0)
img_base64 = base64.b64encode(buffer.getvalue()).decode()
return f"data:image/png;base64,{img_base64}"
def verify_totp(secret, token):
"""Verify a TOTP token"""
totp = pyotp.TOTP(secret)
return totp.verify(token, valid_window=1)
def generate_backup_codes(count=10):
"""Generate backup codes"""
codes = []
for _ in range(count):
code = '-'.join([
secrets.token_hex(2).upper(),
secrets.token_hex(2).upper(),
secrets.token_hex(2).upper()
])
codes.append(code)
return codes
def generate_backup_codes_pdf(username, codes):
"""Generate PDF with backup codes"""
buffer = io.BytesIO()
# Create PDF
doc = SimpleDocTemplate(buffer, pagesize=letter)
story = []
styles = getSampleStyleSheet()
# Custom styles
title_style = ParagraphStyle(
'CustomTitle',
parent=styles['Heading1'],
fontSize=24,
textColor=colors.HexColor('#1D3557'),
spaceAfter=30,
)
subtitle_style = ParagraphStyle(
'CustomSubtitle',
parent=styles['Normal'],
fontSize=12,
textColor=colors.HexColor('#718096'),
spaceAfter=20,
)
# Title
story.append(Paragraph('SoundWave Backup Codes', title_style))
story.append(Paragraph(f'User: {username}', subtitle_style))
story.append(Paragraph(f'Generated: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}', subtitle_style))
story.append(Spacer(1, 0.3 * inch))
# Warning
warning_style = ParagraphStyle(
'Warning',
parent=styles['Normal'],
fontSize=10,
textColor=colors.HexColor('#E53E3E'),
spaceAfter=20,
leftIndent=20,
rightIndent=20,
)
story.append(Paragraph(
'<b>⚠️ IMPORTANT:</b> Store these codes securely. Each code can only be used once. '
'If you lose access to your 2FA device, you can use these codes to log in.',
warning_style
))
story.append(Spacer(1, 0.3 * inch))
# Codes table
data = [['#', 'Backup Code']]
for i, code in enumerate(codes, 1):
data.append([str(i), code])
table = Table(data, colWidths=[0.5 * inch, 3 * inch])
table.setStyle(TableStyle([
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#4ECDC4')),
('TEXTCOLOR', (0, 0), (-1, 0), colors.HexColor('#1D3557')),
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
('FONTSIZE', (0, 0), (-1, 0), 12),
('BOTTOMPADDING', (0, 0), (-1, 0), 12),
('BACKGROUND', (0, 1), (-1, -1), colors.white),
('TEXTCOLOR', (0, 1), (-1, -1), colors.HexColor('#2D3748')),
('FONTNAME', (0, 1), (-1, -1), 'Courier'),
('FONTSIZE', (0, 1), (-1, -1), 11),
('GRID', (0, 0), (-1, -1), 1, colors.HexColor('#E2E8F0')),
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
('LEFTPADDING', (0, 0), (-1, -1), 10),
('RIGHTPADDING', (0, 0), (-1, -1), 10),
('TOPPADDING', (0, 1), (-1, -1), 8),
('BOTTOMPADDING', (0, 1), (-1, -1), 8),
]))
story.append(table)
# Footer
story.append(Spacer(1, 0.5 * inch))
footer_style = ParagraphStyle(
'Footer',
parent=styles['Normal'],
fontSize=9,
textColor=colors.HexColor('#A0AEC0'),
alignment=1, # Center
)
story.append(Paragraph('Keep this document in a safe place', footer_style))
# Build PDF
doc.build(story)
buffer.seek(0)
return buffer