diora-web/accounts/views.py
marwin 817323ad19
All checks were successful
Build and push Docker image / build (push) Successful in 14s
Test / test (push) Successful in 15s
Accounts: fix password change key migration
The old key comparison was wrong — the localStorage key may have been
randomly generated rather than PBKDF2-derived. Fix:
- Add /accounts/check-password/ to validate the old password server-side
  before touching any keys
- Use the localStorage key directly as the old decryption key (it is
  always the correct source of truth, regardless of how it was generated)
- Derive the new key from the new password via PBKDF2

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 16:46:03 +02:00

222 lines
7.1 KiB
Python

import base64
import json
from django.conf import settings
from django.contrib.auth import authenticate, login, get_user_model, update_session_auth_hash
from django.contrib.auth.decorators import login_required
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm, PasswordChangeForm
from django.http import JsonResponse
from django.shortcuts import render, redirect
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods
from radio import lastfm as lastfm_module
User = get_user_model()
# ---------------------------------------------------------------------------
# Register
# ---------------------------------------------------------------------------
def register(request):
if request.user.is_authenticated:
return redirect('index')
if request.method == 'POST':
form = UserCreationForm(request.POST)
if form.is_valid():
user = form.save()
login(request, user)
return redirect('index')
else:
form = UserCreationForm()
return render(request, 'accounts/register.html', {'form': form})
# ---------------------------------------------------------------------------
# Login
# ---------------------------------------------------------------------------
def login_view(request):
if request.user.is_authenticated:
return redirect('index')
if request.method == 'POST':
form = AuthenticationForm(data=request.POST)
if form.is_valid():
user = form.get_user()
login(request, user)
next_url = request.GET.get('next', '/')
return redirect(next_url)
else:
form = AuthenticationForm()
return render(request, 'accounts/login.html', {'form': form})
# ---------------------------------------------------------------------------
# Settings
# ---------------------------------------------------------------------------
@login_required
def settings_view(request):
profile = request.user.profile
if request.method == 'POST':
profile.lastfm_scrobble = 'lastfm_scrobble' in request.POST
profile.save(update_fields=['lastfm_scrobble'])
return redirect('settings')
context = {
'profile': profile,
'has_lastfm': profile.has_lastfm(),
'password_form': PasswordChangeForm(request.user),
}
return render(request, 'accounts/settings.html', context)
# ---------------------------------------------------------------------------
# Last.fm OAuth
# ---------------------------------------------------------------------------
@login_required
def lastfm_connect(request):
callback_url = request.build_absolute_uri('/accounts/lastfm/callback/')
try:
auth_url, token = lastfm_module.get_auth_url(callback_url)
request.session['lastfm_token'] = token
return redirect(auth_url)
except Exception as exc:
return render(request, 'accounts/settings.html', {
'profile': request.user.profile,
'has_lastfm': request.user.profile.has_lastfm(),
'lastfm_error': f"Could not connect to Last.fm: {exc}",
})
@login_required
def lastfm_callback(request):
token = request.session.get('lastfm_token')
if not token:
return redirect('settings')
try:
session_key = lastfm_module.get_session_key(token)
username = lastfm_module.get_username(session_key)
profile = request.user.profile
profile.lastfm_session_key = session_key
profile.lastfm_username = username
profile.save(update_fields=['lastfm_session_key', 'lastfm_username'])
# Clean up session
del request.session['lastfm_token']
except Exception:
pass
return redirect('settings')
@login_required
@csrf_exempt
@require_http_methods(['POST'])
def upload_background(request):
try:
body = json.loads(request.body)
except (json.JSONDecodeError, ValueError):
return JsonResponse({'error': 'invalid JSON'}, status=400)
iv = body.get('iv', '').strip()
ciphertext = body.get('ciphertext', '').strip()
mime_type = body.get('mime_type', '').strip()
file_size = int(body.get('file_size', 0))
if not all([iv, ciphertext, mime_type]):
return JsonResponse({'error': 'iv, ciphertext, mime_type required'}, status=400)
allowed_mimes = {'image/jpeg', 'image/png', 'image/webp'}
if mime_type not in allowed_mimes:
return JsonResponse({'error': 'only jpeg, png, or webp allowed'}, status=400)
if file_size > settings.BG_MAX_BYTES:
return JsonResponse({'error': 'file too large (max 5 MB)'}, status=400)
profile = request.user.profile
profile.background_image_data = ''
profile.background_encrypted = ciphertext
profile.background_iv = iv
profile.background_mime = mime_type
profile.save(update_fields=['background_image_data', 'background_encrypted', 'background_iv', 'background_mime'])
return JsonResponse({'ok': True})
@login_required
@require_http_methods(['POST'])
def delete_background(request):
profile = request.user.profile
profile.background_image_data = ''
profile.background_encrypted = ''
profile.background_iv = ''
profile.background_mime = ''
profile.save(update_fields=['background_image_data', 'background_encrypted', 'background_iv', 'background_mime'])
return redirect('settings')
@login_required
@csrf_exempt
@require_http_methods(['POST'])
def save_focus_station(request):
try:
body = json.loads(request.body)
except (json.JSONDecodeError, ValueError):
return JsonResponse({'error': 'invalid JSON'}, status=400)
url = body.get('url', '').strip()
name = body.get('name', '').strip()
profile = request.user.profile
profile.focus_station_url = url
profile.focus_station_name = name
profile.save(update_fields=['focus_station_url', 'focus_station_name'])
return JsonResponse({'ok': True})
@login_required
@csrf_exempt
@require_http_methods(['POST'])
def check_password(request):
try:
body = json.loads(request.body)
except (json.JSONDecodeError, ValueError):
return JsonResponse({'ok': False}, status=400)
pw = body.get('password', '')
user = authenticate(request, username=request.user.username, password=pw)
return JsonResponse({'ok': user is not None})
@login_required
@require_http_methods(['POST'])
def change_password(request):
form = PasswordChangeForm(request.user, request.POST)
if form.is_valid():
user = form.save()
update_session_auth_hash(request, user)
return redirect('settings')
profile = request.user.profile
return render(request, 'accounts/settings.html', {
'profile': profile,
'has_lastfm': profile.has_lastfm(),
'password_form': form,
'password_form_open': True,
})
@login_required
@require_http_methods(['POST'])
def lastfm_disconnect(request):
profile = request.user.profile
profile.lastfm_session_key = ''
profile.lastfm_username = ''
profile.save(update_fields=['lastfm_session_key', 'lastfm_username'])
return redirect('settings')