You've already forked hotpocket
BTHLABS-50: Safari Web extension
Co-authored-by: Tomek Wójcik <labs@tomekwojcik.pl> Co-committed-by: Tomek Wójcik <labs@tomekwojcik.pl>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
from . import apps # noqaz: F401
|
||||
from . import auth # noqa: F401
|
||||
from . import browse # noqa: F401
|
||||
from . import index # noqa: F401
|
||||
from . import settings # noqa: F401
|
||||
@@ -0,0 +1,167 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
|
||||
from django.contrib import messages
|
||||
import django.db
|
||||
from django.http import HttpRequest, HttpResponse, JsonResponse
|
||||
from django.shortcuts import redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import FormView
|
||||
from django_htmx.http import trigger_client_event
|
||||
|
||||
from hotpocket_backend.apps.accounts.decorators import account_required
|
||||
from hotpocket_backend.apps.accounts.mixins import AccountRequiredMixin
|
||||
from hotpocket_backend.apps.htmx import messages as htmx_messages
|
||||
from hotpocket_backend.apps.ui.constants import UIAccessTokenOriginApp
|
||||
from hotpocket_backend.apps.ui.dto.accounts import AppsBrowseParams
|
||||
from hotpocket_backend.apps.ui.forms.accounts.apps import DeleteForm
|
||||
from hotpocket_backend.apps.ui.services import UIAccessTokensService
|
||||
from hotpocket_soa.dto.accounts import AccessTokenOut, AccessTokensQuery
|
||||
from hotpocket_soa.services import AccessTokensService
|
||||
|
||||
|
||||
class AccessTokenMixin:
|
||||
def get_access_token(self) -> AccessTokenOut:
|
||||
if hasattr(self, '_access_token') is False:
|
||||
setattr(
|
||||
self,
|
||||
'_access_token',
|
||||
UIAccessTokensService().get_or_404(
|
||||
account_uuid=self.request.user.pk, # type: ignore[attr-defined]
|
||||
pk=self.kwargs['pk'], # type: ignore[attr-defined]
|
||||
),
|
||||
)
|
||||
|
||||
return self._access_token # type: ignore[attr-defined]
|
||||
|
||||
|
||||
class DetailView(AccessTokenMixin, AccountRequiredMixin, FormView):
|
||||
def get_context_data(self, **kwargs) -> dict:
|
||||
result = super().get_context_data(**kwargs)
|
||||
|
||||
result.update({
|
||||
'access_token': self.get_access_token(),
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class ConfirmationView(DetailView):
|
||||
def get_initial(self) -> dict:
|
||||
result = super().get_initial()
|
||||
|
||||
access_token: AccessTokenOut = self.get_access_token()
|
||||
|
||||
origin_app = access_token.get_origin_app()
|
||||
if origin_app is not None:
|
||||
origin_app = UIAccessTokenOriginApp[origin_app.value].value
|
||||
|
||||
result.update({
|
||||
'canhazconfirm': 'hai',
|
||||
'origin_app': origin_app or access_token.get_origin_app_id(),
|
||||
'platform': access_token.meta.get('platform', '-'),
|
||||
'version': access_token.meta.get('version', '-'),
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('ui.accounts.apps.browse')
|
||||
|
||||
|
||||
@account_required
|
||||
def index(request: HttpRequest) -> HttpResponse:
|
||||
return redirect(reverse('ui.accounts.apps.browse'))
|
||||
|
||||
|
||||
@account_required
|
||||
def browse(request: HttpRequest) -> HttpResponse:
|
||||
params = AppsBrowseParams.from_request(request=request)
|
||||
|
||||
access_tokens = AccessTokensService().search(
|
||||
query=AccessTokensQuery.model_validate(dict(
|
||||
account_uuid=request.user.pk,
|
||||
before=params.before,
|
||||
)),
|
||||
limit=params.limit,
|
||||
)
|
||||
|
||||
before: uuid.UUID | None = None
|
||||
if len(access_tokens) == params.limit:
|
||||
before = access_tokens[-1].pk
|
||||
|
||||
next_url: str | None = None
|
||||
if before is not None:
|
||||
next_url = reverse('ui.accounts.apps.browse', query=[
|
||||
('before', before),
|
||||
('limit', params.limit),
|
||||
])
|
||||
|
||||
context = {
|
||||
'access_tokens': access_tokens,
|
||||
'params': params,
|
||||
'before': before,
|
||||
'next_url': next_url,
|
||||
}
|
||||
|
||||
if request.htmx:
|
||||
response = render(
|
||||
request,
|
||||
'ui/accounts/partials/apps/apps.html',
|
||||
context,
|
||||
)
|
||||
|
||||
return trigger_client_event(
|
||||
response,
|
||||
'HotPocket:BrowseAccountAppsView:updateLoadMoreButton',
|
||||
{'next_url': next_url},
|
||||
after='swap',
|
||||
)
|
||||
|
||||
return render(
|
||||
request,
|
||||
'ui/accounts/apps/browse.html',
|
||||
context,
|
||||
)
|
||||
|
||||
|
||||
class DeleteView(ConfirmationView):
|
||||
template_name = 'ui/accounts/apps/delete.html'
|
||||
form_class = DeleteForm
|
||||
|
||||
def form_valid(self, form: DeleteForm) -> HttpResponse:
|
||||
with django.db.transaction.atomic():
|
||||
result = AccessTokensService().delete(
|
||||
access_token=self.get_access_token(),
|
||||
)
|
||||
|
||||
if self.request.htmx:
|
||||
response = JsonResponse({
|
||||
'status': 'ok',
|
||||
'result': result,
|
||||
})
|
||||
|
||||
htmx_messages.add_htmx_message(
|
||||
request=self.request,
|
||||
response=response,
|
||||
level=htmx_messages.SUCCESS,
|
||||
message=_('The app auhtorization has been deleted.'),
|
||||
)
|
||||
|
||||
return trigger_client_event(
|
||||
response,
|
||||
'HotPocket:BrowseAccountAppsView:delete',
|
||||
{'pk': self.kwargs['pk']},
|
||||
after='swap',
|
||||
)
|
||||
|
||||
messages.add_message(
|
||||
self.request,
|
||||
messages.SUCCESS,
|
||||
_('The app auhtorization has been deleted.'),
|
||||
)
|
||||
|
||||
return super().form_valid(form)
|
||||
@@ -0,0 +1,66 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import annotations
|
||||
|
||||
from django.contrib.auth import logout as auth_logout
|
||||
from django.contrib.auth.views import LoginView as BaseLoginView
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.shortcuts import render
|
||||
from django.urls import reverse
|
||||
from django.utils.http import url_has_allowed_host_and_scheme
|
||||
from django.views.generic import RedirectView
|
||||
|
||||
from hotpocket_backend.apps.core.conf import settings as django_settings
|
||||
from hotpocket_backend.apps.ui.forms.accounts.auth import LoginForm
|
||||
|
||||
|
||||
class LoginView(BaseLoginView):
|
||||
template_name = 'ui/accounts/login.html'
|
||||
form_class = LoginForm
|
||||
|
||||
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
request.session['post_login_next_url'] = request.GET.get('next', None)
|
||||
request.session.save()
|
||||
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('ui.accounts.post_login')
|
||||
|
||||
|
||||
class PostLoginView(RedirectView):
|
||||
def get_redirect_url(self, *args, **kwargs) -> str:
|
||||
next_url = self.request.session.pop('post_login_next_url', None)
|
||||
self.request.session.save()
|
||||
|
||||
allowed_hosts = None
|
||||
if len(django_settings.ALLOWED_HOSTS) > 0:
|
||||
allowed_hosts = set(filter(
|
||||
lambda value: value != '*',
|
||||
django_settings.ALLOWED_HOSTS,
|
||||
))
|
||||
|
||||
if next_url is not None:
|
||||
next_url_is_safe = url_has_allowed_host_and_scheme(
|
||||
url=next_url,
|
||||
allowed_hosts=allowed_hosts,
|
||||
require_https=self.request.is_secure(),
|
||||
)
|
||||
|
||||
if next_url_is_safe is False:
|
||||
next_url = None
|
||||
|
||||
return next_url or reverse('ui.index.index')
|
||||
|
||||
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
if request.user.is_anonymous is True:
|
||||
raise PermissionDenied('NOPE')
|
||||
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
|
||||
def logout(request: HttpRequest) -> HttpResponse:
|
||||
if request.user.is_authenticated is True:
|
||||
auth_logout(request)
|
||||
|
||||
return render(request, 'ui/accounts/logout.html')
|
||||
@@ -0,0 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import annotations
|
||||
|
||||
from django.http import Http404, HttpRequest, HttpResponse
|
||||
|
||||
from hotpocket_backend.apps.accounts.decorators import account_required
|
||||
|
||||
|
||||
@account_required
|
||||
def browse(request: HttpRequest) -> HttpResponse:
|
||||
raise Http404()
|
||||
@@ -0,0 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import annotations
|
||||
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import reverse
|
||||
|
||||
from hotpocket_backend.apps.accounts.decorators import account_required
|
||||
|
||||
|
||||
@account_required
|
||||
def index(request: HttpRequest) -> HttpResponse:
|
||||
return redirect(reverse('ui.accounts.settings'))
|
||||
@@ -0,0 +1,179 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import annotations
|
||||
|
||||
from django.contrib import messages
|
||||
import django.db
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import FormView
|
||||
|
||||
from hotpocket_backend.apps.accounts.decorators import account_required
|
||||
from hotpocket_backend.apps.accounts.mixins import AccountRequiredMixin
|
||||
from hotpocket_backend.apps.ui.forms.accounts.settings import (
|
||||
FederatedPasswordForm,
|
||||
FederatedProfileForm,
|
||||
PasswordForm,
|
||||
ProfileForm,
|
||||
SettingsForm,
|
||||
)
|
||||
|
||||
|
||||
@account_required
|
||||
def settings(request: HttpRequest) -> HttpResponse:
|
||||
return redirect(reverse('ui.accounts.settings.profile'))
|
||||
|
||||
|
||||
class BaseSettingsView(AccountRequiredMixin, FormView):
|
||||
template_name = 'ui/accounts/settings.html'
|
||||
|
||||
@property
|
||||
def is_federated(self) -> bool:
|
||||
return all((
|
||||
self.request.user.is_anonymous is False,
|
||||
self.request.user.has_usable_password() is False,
|
||||
))
|
||||
|
||||
def get_context_data(self, **kwargs) -> dict:
|
||||
result = super().get_context_data(**kwargs)
|
||||
|
||||
result.update({
|
||||
'is_federated': self.is_federated,
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class ProfileView(BaseSettingsView):
|
||||
def get_form_class(self) -> type[ProfileForm]:
|
||||
if self.is_federated is True:
|
||||
return FederatedProfileForm
|
||||
|
||||
return ProfileForm
|
||||
|
||||
def get_initial(self) -> dict:
|
||||
result = super().get_initial()
|
||||
|
||||
result.update({
|
||||
'username': self.request.user.username,
|
||||
'first_name': self.request.user.first_name,
|
||||
'last_name': self.request.user.last_name,
|
||||
'email': self.request.user.email,
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
def get_context_data(self, **kwargs) -> dict:
|
||||
result = super().get_context_data(**kwargs)
|
||||
|
||||
result.update({
|
||||
'title': _('Profile'),
|
||||
'active_tab': 'profile',
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
def form_valid(self, form: ProfileForm) -> HttpResponse:
|
||||
assert self.is_federated is False, (
|
||||
'Refuse to save profile of a federated account: '
|
||||
f'account=`{self.request.user}`'
|
||||
)
|
||||
|
||||
with django.db.transaction.atomic():
|
||||
self.request.user.first_name = form.cleaned_data['first_name']
|
||||
self.request.user.last_name = form.cleaned_data['last_name']
|
||||
self.request.user.email = form.cleaned_data['email']
|
||||
self.request.user.save()
|
||||
|
||||
messages.add_message(
|
||||
self.request,
|
||||
messages.SUCCESS,
|
||||
message=_('Your profile has been been updated!'),
|
||||
)
|
||||
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('ui.accounts.settings.profile')
|
||||
|
||||
|
||||
class PasswordView(BaseSettingsView):
|
||||
def get_form_class(self) -> type[PasswordForm]:
|
||||
if self.is_federated is True:
|
||||
return FederatedPasswordForm
|
||||
|
||||
return PasswordForm
|
||||
|
||||
def get_form(self,
|
||||
form_class: type[PasswordForm] | None = None,
|
||||
) -> PasswordForm:
|
||||
form_class = form_class or self.get_form_class()
|
||||
return form_class(self.request.user, **self.get_form_kwargs())
|
||||
|
||||
def get_context_data(self, **kwargs) -> dict:
|
||||
result = super().get_context_data(**kwargs)
|
||||
|
||||
result.update({
|
||||
'title': _('Password'),
|
||||
'active_tab': 'password',
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
def form_valid(self, form: PasswordForm) -> HttpResponse:
|
||||
assert self.is_federated is False, (
|
||||
'Refuse to change password of a federated account: '
|
||||
f'account=`{self.request.user}`'
|
||||
)
|
||||
|
||||
with django.db.transaction.atomic():
|
||||
form.set_password_and_save(
|
||||
self.request.user,
|
||||
password_field_name='new_password1',
|
||||
)
|
||||
|
||||
messages.add_message(
|
||||
self.request,
|
||||
messages.SUCCESS,
|
||||
message=_('Your password has been changed!'),
|
||||
)
|
||||
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('ui.accounts.settings.password')
|
||||
|
||||
|
||||
class SettingsView(BaseSettingsView):
|
||||
form_class = SettingsForm
|
||||
|
||||
def get_context_data(self, **kwargs) -> dict:
|
||||
result = super().get_context_data(**kwargs)
|
||||
|
||||
result.update({
|
||||
'title': _('Settings'),
|
||||
'active_tab': 'settings',
|
||||
'is_federated': False,
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
def get_initial(self) -> dict:
|
||||
return self.request.user.settings
|
||||
|
||||
def form_valid(self, form: PasswordForm) -> HttpResponse:
|
||||
with django.db.transaction.atomic():
|
||||
self.request.user.raw_settings = form.cleaned_data
|
||||
self.request.user.save()
|
||||
|
||||
messages.add_message(
|
||||
self.request,
|
||||
messages.SUCCESS,
|
||||
message=_('Your settings have been updated!'),
|
||||
)
|
||||
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('ui.accounts.settings.settings')
|
||||
Reference in New Issue
Block a user