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
This commit is contained in:
commit
51679d1943
254 changed files with 37281 additions and 0 deletions
158
backend/user/two_factor.py
Normal file
158
backend/user/two_factor.py
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
"""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
|
||||
Loading…
Add table
Add a link
Reference in a new issue