mirror of
https://github.com/eitchtee/WYGIWYH.git
synced 2026-07-04 20:11:45 +02:00
4273c541c5
- Personal API tokens (model, user-settings UI, admin, management command, DRF auth class) for non-interactive API access from automations like n8n. Raw token shown once; only a SHA-256 hash is stored; last_used_at writes are throttled. - OAuth2 authorization server via django-oauth-toolkit with authorization server metadata and optional, off-by-default Dynamic Client Registration (RFC 7591), so remote OAuth/MCP clients can authenticate and self-register. - Tests for token auth, DCR gating and the management commands, plus .env.example and README documentation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
287 lines
8.3 KiB
Python
287 lines
8.3 KiB
Python
from apps.common.decorators.demo import disabled_on_demo
|
|
from apps.common.decorators.htmx import only_htmx
|
|
from apps.common.decorators.user import htmx_login_required, is_superuser
|
|
from apps.users.forms import (
|
|
APITokenCreateForm,
|
|
LoginForm,
|
|
UserAddForm,
|
|
UserSettingsForm,
|
|
UserUpdateForm,
|
|
)
|
|
from apps.users.models import APIToken, UserSettings
|
|
from django.contrib import messages
|
|
from django.contrib.auth import get_user_model, logout
|
|
from django.contrib.auth.decorators import login_required
|
|
from django.contrib.auth.views import (
|
|
LoginView,
|
|
)
|
|
from django.core.exceptions import PermissionDenied
|
|
from django.http import HttpResponse
|
|
from django.shortcuts import get_object_or_404, redirect, render
|
|
from django.urls import reverse
|
|
from django.utils import timezone
|
|
from django.utils.translation import gettext_lazy as _
|
|
from django.views.decorators.http import require_http_methods
|
|
|
|
|
|
def logout_view(request):
|
|
logout(request)
|
|
return redirect(reverse("login"))
|
|
|
|
|
|
@htmx_login_required
|
|
def index(request):
|
|
if request.user.settings.start_page == UserSettings.StartPage.MONTHLY:
|
|
return redirect(reverse("monthly_index"))
|
|
elif request.user.settings.start_page == UserSettings.StartPage.YEARLY_ACCOUNT:
|
|
return redirect(reverse("yearly_index_account"))
|
|
elif request.user.settings.start_page == UserSettings.StartPage.YEARLY_CURRENCY:
|
|
return redirect(reverse("yearly_index_currency"))
|
|
elif request.user.settings.start_page == UserSettings.StartPage.NETWORTH_CURRENT:
|
|
return redirect(reverse("net_worth_current"))
|
|
elif request.user.settings.start_page == UserSettings.StartPage.NETWORTH_PROJECTED:
|
|
return redirect(reverse("net_worth_projected"))
|
|
elif request.user.settings.start_page == UserSettings.StartPage.ALL_TRANSACTIONS:
|
|
return redirect(reverse("transactions_all_index"))
|
|
elif request.user.settings.start_page == UserSettings.StartPage.CALENDAR:
|
|
return redirect(reverse("calendar_index"))
|
|
else:
|
|
return redirect(reverse("monthly_index"))
|
|
|
|
|
|
class UserLoginView(LoginView):
|
|
form_class = LoginForm
|
|
template_name = "users/login.html"
|
|
redirect_authenticated_user = True
|
|
|
|
|
|
@only_htmx
|
|
@htmx_login_required
|
|
def toggle_amount_visibility(request):
|
|
user_settings, created = UserSettings.objects.get_or_create(user=request.user)
|
|
current_hide_amounts = user_settings.hide_amounts
|
|
new_hide_amounts = not current_hide_amounts
|
|
|
|
user_settings.hide_amounts = new_hide_amounts
|
|
user_settings.save()
|
|
|
|
if new_hide_amounts is True:
|
|
messages.info(request, _("Transaction amounts are now hidden"))
|
|
response = render(request, "users/generic/show_amounts.html")
|
|
else:
|
|
messages.info(request, _("Transaction amounts are now displayed"))
|
|
response = render(request, "users/generic/hide_amounts.html")
|
|
|
|
response.headers["HX-Trigger"] = "updated"
|
|
return response
|
|
|
|
|
|
@only_htmx
|
|
@htmx_login_required
|
|
def toggle_sound_playing(request):
|
|
user_settings, created = UserSettings.objects.get_or_create(user=request.user)
|
|
current_mute_sounds = user_settings.mute_sounds
|
|
new_mute_sounds = not current_mute_sounds
|
|
|
|
user_settings.mute_sounds = new_mute_sounds
|
|
user_settings.save()
|
|
|
|
if new_mute_sounds is True:
|
|
messages.info(request, _("Sounds are now muted"))
|
|
response = render(request, "users/generic/play_sounds.html")
|
|
else:
|
|
messages.info(request, _("Sounds will now play"))
|
|
response = render(request, "users/generic/mute_sounds.html")
|
|
|
|
response.headers["HX-Trigger"] = "updated"
|
|
return response
|
|
|
|
|
|
@only_htmx
|
|
@htmx_login_required
|
|
def update_settings(request):
|
|
user_settings = request.user.settings
|
|
|
|
if request.method == "POST":
|
|
form = UserSettingsForm(request.POST, instance=user_settings)
|
|
if form.is_valid():
|
|
form.save()
|
|
messages.success(request, _("Your settings have been updated"))
|
|
return HttpResponse(
|
|
status=204,
|
|
headers={"HX-Refresh": "true"},
|
|
)
|
|
else:
|
|
form = UserSettingsForm(instance=user_settings)
|
|
|
|
return render(
|
|
request,
|
|
"users/fragments/user_settings.html",
|
|
{
|
|
"form": form,
|
|
"api_token_form": APITokenCreateForm(),
|
|
"api_tokens": request.user.api_tokens.all(),
|
|
},
|
|
)
|
|
|
|
|
|
def _render_api_tokens(request, *, form=None, raw_token=None):
|
|
return render(
|
|
request,
|
|
"users/fragments/api_tokens.html",
|
|
{
|
|
"api_token_form": form or APITokenCreateForm(),
|
|
"api_tokens": request.user.api_tokens.all(),
|
|
"raw_token": raw_token,
|
|
},
|
|
)
|
|
|
|
|
|
@only_htmx
|
|
@htmx_login_required
|
|
@require_http_methods(["POST"])
|
|
def api_token_add(request):
|
|
form = APITokenCreateForm(request.POST)
|
|
if form.is_valid():
|
|
_token, raw_token = form.save(user=request.user)
|
|
messages.success(request, _("API token created successfully"))
|
|
return _render_api_tokens(
|
|
request,
|
|
form=APITokenCreateForm(),
|
|
raw_token=raw_token,
|
|
)
|
|
|
|
return _render_api_tokens(request, form=form)
|
|
|
|
|
|
@only_htmx
|
|
@htmx_login_required
|
|
@require_http_methods(["DELETE"])
|
|
def api_token_revoke(request, token_id):
|
|
token = get_object_or_404(APIToken, id=token_id, user=request.user)
|
|
if token.revoked_at is None:
|
|
token.revoked_at = timezone.now()
|
|
token.save(update_fields=["revoked_at"])
|
|
messages.success(request, _("API token revoked successfully"))
|
|
return _render_api_tokens(request)
|
|
|
|
|
|
@only_htmx
|
|
@htmx_login_required
|
|
@require_http_methods(["GET"])
|
|
def toggle_sidebar_status(request):
|
|
if not request.session.get("sidebar_status"):
|
|
request.session["sidebar_status"] = "floating"
|
|
|
|
if request.session["sidebar_status"] == "floating":
|
|
request.session["sidebar_status"] = "fixed"
|
|
elif request.session["sidebar_status"] == "fixed":
|
|
request.session["sidebar_status"] = "floating"
|
|
else:
|
|
request.session["sidebar_status"] = "fixed"
|
|
|
|
return HttpResponse(
|
|
status=204,
|
|
)
|
|
|
|
|
|
@htmx_login_required
|
|
@require_http_methods(["GET"])
|
|
def toggle_theme(request):
|
|
if not request.session.get("theme"):
|
|
request.session["theme"] = "wygiwyh_dark"
|
|
|
|
if request.session["theme"] == "wygiwyh_dark":
|
|
request.session["theme"] = "wygiwyh_light"
|
|
elif request.session["theme"] == "wygiwyh_light":
|
|
request.session["theme"] = "wygiwyh_dark"
|
|
else:
|
|
request.session["theme"] = "wygiwyh_light"
|
|
|
|
return HttpResponse(
|
|
status=204,
|
|
)
|
|
|
|
|
|
@htmx_login_required
|
|
@is_superuser
|
|
@require_http_methods(["GET"])
|
|
def users_index(request):
|
|
return render(
|
|
request,
|
|
"users/pages/index.html",
|
|
)
|
|
|
|
|
|
@only_htmx
|
|
@htmx_login_required
|
|
@is_superuser
|
|
@require_http_methods(["GET"])
|
|
def users_list(request):
|
|
users = get_user_model().objects.all().order_by("id")
|
|
|
|
return render(
|
|
request,
|
|
"users/fragments/list.html",
|
|
{"users": users},
|
|
)
|
|
|
|
|
|
@only_htmx
|
|
@htmx_login_required
|
|
@is_superuser
|
|
@require_http_methods(["GET", "POST"])
|
|
def user_add(request):
|
|
if request.method == "POST":
|
|
form = UserAddForm(request.POST)
|
|
if form.is_valid():
|
|
form.save()
|
|
messages.success(request, _("Item added successfully"))
|
|
|
|
return HttpResponse(
|
|
status=204,
|
|
headers={
|
|
"HX-Trigger": "updated, hide_offcanvas",
|
|
},
|
|
)
|
|
else:
|
|
form = UserAddForm()
|
|
|
|
return render(
|
|
request,
|
|
"users/fragments/add.html",
|
|
{"form": form},
|
|
)
|
|
|
|
|
|
@only_htmx
|
|
@htmx_login_required
|
|
@disabled_on_demo
|
|
@require_http_methods(["GET", "POST"])
|
|
def user_edit(request, pk):
|
|
user = get_object_or_404(get_user_model(), id=pk)
|
|
|
|
if not request.user.is_superuser and user != request.user:
|
|
raise PermissionDenied
|
|
|
|
if request.method == "POST":
|
|
form = UserUpdateForm(request.POST, instance=user)
|
|
if form.is_valid():
|
|
form.save()
|
|
messages.success(request, _("Item updated successfully"))
|
|
|
|
return HttpResponse(
|
|
status=204,
|
|
headers={
|
|
"HX-Trigger": "updated, hide_offcanvas",
|
|
},
|
|
)
|
|
else:
|
|
form = UserUpdateForm(instance=user)
|
|
|
|
return render(
|
|
request,
|
|
"users/fragments/edit.html",
|
|
{"form": form, "user": user},
|
|
)
|