*: rewrite user settings to use a single page

This commit is contained in:
Jens Langhammer 2020-11-22 20:30:26 +01:00
parent be8cc77086
commit fcf763ed3e
21 changed files with 218 additions and 242 deletions

View File

@ -18,7 +18,7 @@ from structlog import get_logger
from passbook.core.exceptions import PropertyMappingExpressionException from passbook.core.exceptions import PropertyMappingExpressionException
from passbook.core.signals import password_changed from passbook.core.signals import password_changed
from passbook.core.types import UILoginButton, UIUserSettings from passbook.core.types import UILoginButton
from passbook.flows.models import Flow from passbook.flows.models import Flow
from passbook.lib.models import CreatedUpdatedModel from passbook.lib.models import CreatedUpdatedModel
from passbook.policies.models import PolicyBindingModel from passbook.policies.models import PolicyBindingModel
@ -249,9 +249,9 @@ class Source(PolicyBindingModel):
return None return None
@property @property
def ui_user_settings(self) -> Optional[UIUserSettings]: def ui_user_settings(self) -> Optional[str]:
"""Entrypoint to integrate with User settings. Can either return None if no """Entrypoint to integrate with User settings. Can either return None if no
user settings are available, or an instanace of UIUserSettings.""" user settings are available, or a string with the URL to fetch."""
return None return None
def __str__(self): def __str__(self):

View File

@ -1,71 +0,0 @@
{% extends "base/page.html" %}
{% load i18n %}
{% load passbook_is_active %}
{% load static %}
{% load passbook_user_settings %}
{% block page_content %}
<div class="pf-c-page__sidebar">
<div class="pf-c-page__sidebar-body">
<nav class="pf-c-nav" id="page-default-nav-example-primary-nav" aria-label="Global">
<section class="pf-c-nav__section">
<h2 class="pf-c-nav__section-title">{% trans 'General Settings' %}</h2>
<ul class="pf-c-nav__list">
<li class="pf-c-nav__item">
<a href="{% url 'passbook_core:user-settings' %}"
class="pf-c-nav__link {% is_active 'passbook_core:user-settings' %}">{% trans 'User Details' %}</a>
</li>
<li class="pf-c-nav__item">
<a href="{% url 'passbook_core:user-tokens' %}"
class="pf-c-nav__link {% is_active 'passbook_core:user-tokens' 'passbook_core:user-tokens-create' 'passbook_core:user-tokens-update' 'passbook_core:user-tokens-delete' %}">{% trans 'Tokens' %}</a>
</li>
</ul>
</section>
{% user_stages as user_stages_loc %}
{% if user_stages_loc %}
<section class="pf-c-nav__section">
<h2 class="pf-c-nav__section-title">{% trans 'Stages' %}</h2>
<ul class="pf-c-nav__list">
{% for stage in user_stages_loc %}
<li class="pf-c-nav__item">
<a href="{{ stage.url }}" class="pf-c-nav__link {% if stage.url == request.get_full_path %} pf-m-current {% endif %}">
{{ stage.name }}
</a>
</li>
{% endfor %}
</ul>
</section>
{% endif %}
{% user_sources as user_sources_loc %}
{% if user_sources_loc %}
<section class="pf-c-nav__section">
<h2 class="pf-c-nav__section-title">{% trans 'Sources' %}</h2>
<ul class="pf-c-nav__list">
{% for source in user_sources_loc %}
<li class="pf-c-nav__item">
<a href="{{ source.url }}"
class="pf-c-nav__link {% if source.url == request.get_full_path %} pf-m-current {% endif %}">
{{ source.name }}
</a>
</li>
{% endfor %}
</ul>
</section>
{% endif %}
</nav>
</div>
</div>
<main role="main" class="pf-c-page__main" tabindex="-1" id="main-content">
{% block content %}
<section class="pf-c-page__main-section">
<div class="pf-u-display-flex pf-u-justify-content-center">
<div class="pf-u-w-75">
{% block page %}
{% endblock %}
</div>
</div>
</section>
{% endblock %}
</main>
{% endblock %}

View File

@ -1,9 +1,21 @@
{% extends "user/base.html" %}
{% load i18n %} {% load i18n %}
{% load passbook_user_settings %}
{% block page %} <div class="pf-c-page">
<div class="pf-c-card"> <main role="main" class="pf-c-page__main" tabindex="-1">
<section class="pf-c-page__main-section pf-m-light">
<div class="pf-c-content">
<h1>
<i class="pf-icon pf-icon-user"></i>
{% trans 'User Settings' %}
</h1>
<p>{% trans "Configure settings relevant to your user profile." %}</p>
</div>
</section>
<section class="pf-c-page__main-section">
<div class="pf-u-display-flex pf-u-justify-content-center">
<div class="pf-u-w-75">
<div class="pf-c-card">
<div class="pf-c-card__header pf-c-title pf-m-md"> <div class="pf-c-card__header pf-c-title pf-m-md">
{% trans 'Update details' %} {% trans 'Update details' %}
</div> </div>
@ -17,12 +29,50 @@
<div class="pf-c-form__actions"> <div class="pf-c-form__actions">
<input class="pf-c-button pf-m-primary" type="submit" value="{% trans 'Update' %}" /> <input class="pf-c-button pf-m-primary" type="submit" value="{% trans 'Update' %}" />
{% if unenrollment_enabled %} {% if unenrollment_enabled %}
<a class="pf-c-button pf-m-danger" href="{% url 'passbook_flows:default-unenrollment' %}?back={{ request.get_full_path }}">{% trans "Delete account" %}</a> <a class="pf-c-button pf-m-danger"
href="{% url 'passbook_flows:default-unenrollment' %}?back={{ request.get_full_path }}">{% trans "Delete account" %}</a>
{% endif %} {% endif %}
</div> </div>
</div> </div>
</div> </div>
</form> </form>
</div> </div>
</div>
</div>
</div>
</section>
<section class="pf-c-page__main-section">
<div class="pf-u-display-flex pf-u-justify-content-center">
<div class="pf-u-w-75">
<pb-site-shell url="{% url 'passbook_core:user-tokens' %}">
<div slot="body"></div>
</pb-site-shell>
</div>
</div>
</section>
{% user_stages as user_stages_loc %}
{% for stage in user_stages_loc %}
<section class="pf-c-page__main-section">
<div class="pf-u-display-flex pf-u-justify-content-center">
<div class="pf-u-w-75">
<pb-site-shell url="{{ stage }}">
<div slot="body"></div>
</pb-site-shell>
</div>
</div>
</section>
{% endfor %}
{% user_sources as user_sources_loc %}
{% for source in user_sources_loc %}
<section class="pf-c-page__main-section">
<div class="pf-u-display-flex pf-u-justify-content-center">
<div class="pf-u-w-75">
<pb-site-shell url="{{ source }}">
<div slot="body"></div>
</pb-site-shell>
</div>
</div>
</section>
{% endfor %}
</main>
</div> </div>
{% endblock %}

View File

@ -1,21 +1,13 @@
{% extends "user/base.html" %}
{% load i18n %} {% load i18n %}
{% load passbook_utils %}
{% block content %} <div class="pf-c-card">
<section class="pf-c-page__main-section pf-m-light"> <div class="pf-c-card__header pf-c-title pf-m-md">
<div class="pf-c-content">
<h1> <h1>
<i class="pf-icon pf-icon-users"></i> <i class="pf-icon pf-icon-users"></i>
{% trans 'Tokens' %} {% trans 'Manage Tokens' %}
</h1> </h1>
<p>{% trans "Tokens can be used to access passbook's API." %} <p>{% trans "Tokens can be used to access passbook's API." %}</p>
</p>
</div> </div>
</section>
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
<div class="pf-c-card">
{% if object_list %} {% if object_list %}
<div class="pf-c-toolbar"> <div class="pf-c-toolbar">
<div class="pf-c-toolbar__content"> <div class="pf-c-toolbar__content">
@ -86,6 +78,4 @@
</div> </div>
</div> </div>
{% endif %} {% endif %}
</div> </div>
</section>
{% endblock %}

View File

@ -1,11 +1,10 @@
"""passbook user settings template tags""" """passbook user settings template tags"""
from typing import Iterable, List from typing import Iterable
from django import template from django import template
from django.template.context import RequestContext from django.template.context import RequestContext
from passbook.core.models import Source from passbook.core.models import Source
from passbook.core.types import UIUserSettings
from passbook.flows.models import Stage from passbook.flows.models import Stage
from passbook.policies.engine import PolicyEngine from passbook.policies.engine import PolicyEngine
@ -14,26 +13,26 @@ register = template.Library()
@register.simple_tag(takes_context=True) @register.simple_tag(takes_context=True)
# pylint: disable=unused-argument # pylint: disable=unused-argument
def user_stages(context: RequestContext) -> List[UIUserSettings]: def user_stages(context: RequestContext) -> list[str]:
"""Return list of all stages which apply to user""" """Return list of all stages which apply to user"""
_all_stages: Iterable[Stage] = Stage.objects.all().select_subclasses() _all_stages: Iterable[Stage] = Stage.objects.all().select_subclasses()
matching_stages: List[UIUserSettings] = [] matching_stages: list[str] = []
for stage in _all_stages: for stage in _all_stages:
user_settings = stage.ui_user_settings user_settings = stage.ui_user_settings
if not user_settings: if not user_settings:
continue continue
matching_stages.append(user_settings) matching_stages.append(user_settings)
return sorted(matching_stages, key=lambda x: x.name) return matching_stages
@register.simple_tag(takes_context=True) @register.simple_tag(takes_context=True)
def user_sources(context: RequestContext) -> List[UIUserSettings]: def user_sources(context: RequestContext) -> list[str]:
"""Return a list of all sources which are enabled for the user""" """Return a list of all sources which are enabled for the user"""
user = context.get("request").user user = context.get("request").user
_all_sources: Iterable[Source] = Source.objects.filter( _all_sources: Iterable[Source] = Source.objects.filter(
enabled=True enabled=True
).select_subclasses() ).select_subclasses()
matching_sources: List[UIUserSettings] = [] matching_sources: list[str] = []
for source in _all_sources: for source in _all_sources:
user_settings = source.ui_user_settings user_settings = source.ui_user_settings
if not user_settings: if not user_settings:
@ -42,4 +41,4 @@ def user_sources(context: RequestContext) -> List[UIUserSettings]:
policy_engine.build() policy_engine.build()
if policy_engine.passing: if policy_engine.passing:
matching_sources.append(user_settings) matching_sources.append(user_settings)
return sorted(matching_sources, key=lambda x: x.name) return matching_sources

View File

@ -3,17 +3,9 @@ from dataclasses import dataclass
from typing import Optional from typing import Optional
@dataclass
class UIUserSettings:
"""Dataclass for Stage and Source's user_settings"""
name: str
url: str
@dataclass @dataclass
class UILoginButton: class UILoginButton:
"""Dataclass for Source's ui_ui_login_button""" """Dataclass for Source's ui_login_button"""
# Name, ran through i18n # Name, ran through i18n
name: str name: str

View File

@ -10,7 +10,6 @@ from model_utils.managers import InheritanceManager
from rest_framework.serializers import BaseSerializer from rest_framework.serializers import BaseSerializer
from structlog import get_logger from structlog import get_logger
from passbook.core.types import UIUserSettings
from passbook.lib.models import InheritanceForeignKey, SerializerModel from passbook.lib.models import InheritanceForeignKey, SerializerModel
from passbook.policies.models import PolicyBindingModel from passbook.policies.models import PolicyBindingModel
@ -64,9 +63,9 @@ class Stage(SerializerModel):
raise NotImplementedError raise NotImplementedError
@property @property
def ui_user_settings(self) -> Optional[UIUserSettings]: def ui_user_settings(self) -> Optional[str]:
"""Entrypoint to integrate with User settings. Can either return None if no """Entrypoint to integrate with User settings. Can either return None if no
user settings are available, or an instanace of UIUserSettings.""" user settings are available, or a string with the URL to fetch."""
return None return None
def __str__(self): def __str__(self):

View File

@ -7,7 +7,7 @@ from django.urls import reverse, reverse_lazy
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from passbook.core.models import Source, UserSourceConnection from passbook.core.models import Source, UserSourceConnection
from passbook.core.types import UILoginButton, UIUserSettings from passbook.core.types import UILoginButton
class OAuthSource(Source): class OAuthSource(Source):
@ -66,12 +66,9 @@ class OAuthSource(Source):
return f"Callback URL: <pre>{url}</pre>" return f"Callback URL: <pre>{url}</pre>"
@property @property
def ui_user_settings(self) -> Optional[UIUserSettings]: def ui_user_settings(self) -> Optional[str]:
view_name = "passbook_sources_oauth:oauth-client-user" view_name = "passbook_sources_oauth:oauth-client-user"
return UIUserSettings( return reverse(view_name, kwargs={"source_slug": self.slug})
name=self.name,
url=reverse(view_name, kwargs={"source_slug": self.slug}),
)
def __str__(self) -> str: def __str__(self) -> str:
return f"OAuth Source {self.name}" return f"OAuth Source {self.name}"

View File

@ -1,9 +1,5 @@
{% extends "user/base.html" %}
{% load passbook_utils %}
{% load i18n %} {% load i18n %}
{% block page %}
<div class="pf-c-card"> <div class="pf-c-card">
<div class="pf-c-card__header pf-c-title pf-m-md"> <div class="pf-c-card__header pf-c-title pf-m-md">
{% blocktrans with source_name=source.name %} {% blocktrans with source_name=source.name %}
@ -26,4 +22,3 @@
{% endif %} {% endif %}
</div> </div>
</div> </div>
{% endblock %}

View File

@ -8,7 +8,6 @@ from django.utils.translation import gettext_lazy as _
from django.views import View from django.views import View
from rest_framework.serializers import BaseSerializer from rest_framework.serializers import BaseSerializer
from passbook.core.types import UIUserSettings
from passbook.flows.models import ConfigurableStage, Stage from passbook.flows.models import ConfigurableStage, Stage
@ -36,13 +35,10 @@ class OTPStaticStage(ConfigurableStage, Stage):
return OTPStaticStageForm return OTPStaticStageForm
@property @property
def ui_user_settings(self) -> Optional[UIUserSettings]: def ui_user_settings(self) -> Optional[str]:
return UIUserSettings( return reverse(
name="Static OTP",
url=reverse(
"passbook_stages_otp_static:user-settings", "passbook_stages_otp_static:user-settings",
kwargs={"stage_uuid": self.stage_uuid}, kwargs={"stage_uuid": self.stage_uuid},
),
) )
def __str__(self) -> str: def __str__(self) -> str:

View File

@ -1,9 +1,5 @@
{% extends "user/base.html" %}
{% load passbook_utils %}
{% load i18n %} {% load i18n %}
{% block page %}
<div class="pf-c-card"> <div class="pf-c-card">
<div class="pf-c-card__header pf-c-title pf-m-md"> <div class="pf-c-card__header pf-c-title pf-m-md">
{% trans "Static One-Time Passwords" %} {% trans "Static One-Time Passwords" %}
@ -33,4 +29,3 @@
{% endif %} {% endif %}
</div> </div>
</div> </div>
{% endblock %}

View File

@ -8,7 +8,6 @@ from django.utils.translation import gettext_lazy as _
from django.views import View from django.views import View
from rest_framework.serializers import BaseSerializer from rest_framework.serializers import BaseSerializer
from passbook.core.types import UIUserSettings
from passbook.flows.models import ConfigurableStage, Stage from passbook.flows.models import ConfigurableStage, Stage
@ -43,13 +42,10 @@ class OTPTimeStage(ConfigurableStage, Stage):
return OTPTimeStageForm return OTPTimeStageForm
@property @property
def ui_user_settings(self) -> Optional[UIUserSettings]: def ui_user_settings(self) -> Optional[str]:
return UIUserSettings( return reverse(
name="Time-based OTP",
url=reverse(
"passbook_stages_otp_time:user-settings", "passbook_stages_otp_time:user-settings",
kwargs={"stage_uuid": self.stage_uuid}, kwargs={"stage_uuid": self.stage_uuid},
),
) )
def __str__(self) -> str: def __str__(self) -> str:

View File

@ -1,9 +1,5 @@
{% extends "user/base.html" %}
{% load passbook_utils %}
{% load i18n %} {% load i18n %}
{% block page %}
<div class="pf-c-card"> <div class="pf-c-card">
<div class="pf-c-card__header pf-c-title pf-m-md"> <div class="pf-c-card__header pf-c-title pf-m-md">
{% trans "Time-based One-Time Passwords" %} {% trans "Time-based One-Time Passwords" %}
@ -30,4 +26,3 @@
</p> </p>
</div> </div>
</div> </div>
{% endblock %}

View File

@ -8,3 +8,4 @@ class PassbookStagePasswordConfig(AppConfig):
name = "passbook.stages.password" name = "passbook.stages.password"
label = "passbook_stages_password" label = "passbook_stages_password"
verbose_name = "passbook Stages.Password" verbose_name = "passbook Stages.Password"
mountpoint = "-/user/password/"

View File

@ -4,15 +4,12 @@ from typing import Optional, Type
from django.contrib.postgres.fields import ArrayField from django.contrib.postgres.fields import ArrayField
from django.db import models from django.db import models
from django.forms import ModelForm from django.forms import ModelForm
from django.shortcuts import reverse
from django.utils.http import urlencode
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views import View from django.views import View
from rest_framework.serializers import BaseSerializer from rest_framework.serializers import BaseSerializer
from django.shortcuts import reverse
from passbook.core.types import UIUserSettings
from passbook.flows.models import ConfigurableStage, Stage from passbook.flows.models import ConfigurableStage, Stage
from passbook.flows.views import NEXT_ARG_NAME
class PasswordStage(ConfigurableStage, Stage): class PasswordStage(ConfigurableStage, Stage):
@ -51,12 +48,10 @@ class PasswordStage(ConfigurableStage, Stage):
return PasswordStageForm return PasswordStageForm
@property @property
def ui_user_settings(self) -> Optional[UIUserSettings]: def ui_user_settings(self) -> Optional[str]:
if not self.configure_flow: if not self.configure_flow:
return None return None
base_url = reverse("passbook_flows:configure", kwargs={"stage_uuid": self.pk}) return reverse("passbook_stages_password:user-settings", kwargs={"stage_uuid": self.pk})
args = urlencode({NEXT_ARG_NAME: reverse("passbook_core:user-settings")})
return UIUserSettings(name=_("Change password"), url=f"{base_url}?{args}")
def __str__(self): def __str__(self):
return f"Password Stage {self.name}" return f"Password Stage {self.name}"

View File

@ -52,7 +52,7 @@ class PasswordStageView(FormView, StageView):
"""Authentication stage which authenticates against django's AuthBackend""" """Authentication stage which authenticates against django's AuthBackend"""
form_class = PasswordForm form_class = PasswordForm
template_name = "stages/password/backend.html" template_name = "stages/password/flow-form.html"
def get_form(self, form_class=None) -> PasswordForm: def get_form(self, form_class=None) -> PasswordForm:
form = super().get_form(form_class=form_class) form = super().get_form(form_class=form_class)

View File

@ -0,0 +1,17 @@
{% extends "base/page.html" %}
{% load i18n %}
{% load passbook_utils %}
{% block body %}
<div class="pf-c-card">
<div class="pf-c-card__header pf-c-title pf-m-md">
{% trans 'Reset your password' %}
</div>
<div class="pf-c-card__body">
<a class="pf-c-button pf-m-primary" href="{{ url }}">
{% trans 'Change password' %}
</a>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,10 @@
"""Password stage urls"""
from django.urls import path
from passbook.stages.password.views import UserSettingsCardView
urlpatterns = [
path(
"<uuid:stage_uuid>/change-card/", UserSettingsCardView.as_view(), name="user-settings"
),
]

View File

@ -0,0 +1,20 @@
from typing import Any
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import TemplateView
from django.shortcuts import reverse
from django.utils.http import urlencode
from passbook.flows.views import NEXT_ARG_NAME
class UserSettingsCardView(LoginRequiredMixin, TemplateView):
template_name = "stages/password/user-settings-card.html"
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
base_url = reverse("passbook_flows:configure", kwargs={"stage_uuid": self.kwargs["stage_uuid"]})
args = urlencode({NEXT_ARG_NAME: reverse("passbook_core:user-settings")})
kwargs = super().get_context_data(**kwargs)
kwargs["url"] = f"{base_url}?{args}"
return kwargs

File diff suppressed because one or more lines are too long