*: 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.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.lib.models import CreatedUpdatedModel
from passbook.policies.models import PolicyBindingModel
@ -249,9 +249,9 @@ class Source(PolicyBindingModel):
return None
@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
user settings are available, or an instanace of UIUserSettings."""
user settings are available, or a string with the URL to fetch."""
return None
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,28 +1,78 @@
{% extends "user/base.html" %}
{% load i18n %}
{% load passbook_user_settings %}
{% block page %}
<div class="pf-c-card">
<div class="pf-c-card__header pf-c-title pf-m-md">
{% trans 'Update details' %}
</div>
<div class="pf-c-card__body">
<form action="" method="post" class="pf-c-form pf-m-horizontal">
{% include 'partials/form_horizontal.html' with form=form %}
{% block beneath_form %}
{% endblock %}
<div class="pf-c-form__group pf-m-action">
<div class="pf-c-form__horizontal-group">
<div class="pf-c-form__actions">
<input class="pf-c-button pf-m-primary" type="submit" value="{% trans 'Update' %}" />
{% 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>
{% endif %}
<div class="pf-c-page">
<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">
{% trans 'Update details' %}
</div>
<div class="pf-c-card__body">
<form action="" method="post" class="pf-c-form pf-m-horizontal">
{% include 'partials/form_horizontal.html' with form=form %}
{% block beneath_form %}
{% endblock %}
<div class="pf-c-form__group pf-m-action">
<div class="pf-c-form__horizontal-group">
<div class="pf-c-form__actions">
<input class="pf-c-button pf-m-primary" type="submit" value="{% trans 'Update' %}" />
{% 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>
{% endif %}
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</form>
</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>
{% endblock %}

View File

@ -1,91 +1,81 @@
{% extends "user/base.html" %}
{% load i18n %}
{% load passbook_utils %}
{% block content %}
<section class="pf-c-page__main-section pf-m-light">
<div class="pf-c-content">
<div class="pf-c-card">
<div class="pf-c-card__header pf-c-title pf-m-md">
<h1>
<i class="pf-icon pf-icon-users"></i>
{% trans 'Tokens' %}
{% trans 'Manage Tokens' %}
</h1>
<p>{% trans "Tokens can be used to access passbook's API." %}
</p>
<p>{% trans "Tokens can be used to access passbook's API." %}</p>
</div>
</section>
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
<div class="pf-c-card">
{% if object_list %}
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
{% include 'partials/toolbar_search.html' %}
<div class="pf-c-toolbar__bulk-select">
<a href="{% url 'passbook_core:user-tokens-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
</div>
{% include 'partials/pagination.html' %}
{% if object_list %}
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
{% include 'partials/toolbar_search.html' %}
<div class="pf-c-toolbar__bulk-select">
<a href="{% url 'passbook_core:user-tokens-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
</div>
</div>
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
<thead>
<tr role="row">
<th role="columnheader" scope="col">{% trans 'Identifier' %}</th>
<th role="columnheader" scope="col">{% trans 'Expires?' %}</th>
<th role="columnheader" scope="col">{% trans 'Expiry Date' %}</th>
<th role="columnheader" scope="col">{% trans 'Description' %}</th>
<th role="cell"></th>
</tr>
</thead>
<tbody role="rowgroup">
{% for token in object_list %}
<tr role="row">
<th role="columnheader">
<div>{{ token.identifier }}</div>
</th>
<td role="cell">
<span>
{{ token.expiring|yesno:"Yes,No" }}
</span>
</td>
<td role="cell">
<span>
{% if not token.expiring %}
-
{% else %}
{{ token.expires }}
{% endif %}
</span>
</td>
<td role="cell">
<span>
{{ token.description }}
</span>
</td>
<td>
<a class="pf-c-button pf-m-secondary" href="{% url 'passbook_core:user-tokens-update' identifier=token.identifier %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>
<a class="pf-c-button pf-m-danger" href="{% url 'passbook_core:user-tokens-delete' identifier=token.identifier %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="pf-c-pagination pf-m-bottom">
{% include 'partials/pagination.html' %}
</div>
{% else %}
<div class="pf-c-empty-state">
<div class="pf-c-empty-state__content">
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
<h1 class="pf-c-title pf-m-lg">
{% trans 'No Tokens.' %}
</h1>
<div class="pf-c-empty-state__body">
{% trans 'Currently no tokens exist. Click the button below to create one.' %}
</div>
<a href="{% url 'passbook_core:user-tokens-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
</div>
</div>
{% endif %}
</div>
</section>
{% endblock %}
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
<thead>
<tr role="row">
<th role="columnheader" scope="col">{% trans 'Identifier' %}</th>
<th role="columnheader" scope="col">{% trans 'Expires?' %}</th>
<th role="columnheader" scope="col">{% trans 'Expiry Date' %}</th>
<th role="columnheader" scope="col">{% trans 'Description' %}</th>
<th role="cell"></th>
</tr>
</thead>
<tbody role="rowgroup">
{% for token in object_list %}
<tr role="row">
<th role="columnheader">
<div>{{ token.identifier }}</div>
</th>
<td role="cell">
<span>
{{ token.expiring|yesno:"Yes,No" }}
</span>
</td>
<td role="cell">
<span>
{% if not token.expiring %}
-
{% else %}
{{ token.expires }}
{% endif %}
</span>
</td>
<td role="cell">
<span>
{{ token.description }}
</span>
</td>
<td>
<a class="pf-c-button pf-m-secondary" href="{% url 'passbook_core:user-tokens-update' identifier=token.identifier %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>
<a class="pf-c-button pf-m-danger" href="{% url 'passbook_core:user-tokens-delete' identifier=token.identifier %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="pf-c-pagination pf-m-bottom">
{% include 'partials/pagination.html' %}
</div>
{% else %}
<div class="pf-c-empty-state">
<div class="pf-c-empty-state__content">
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
<h1 class="pf-c-title pf-m-lg">
{% trans 'No Tokens.' %}
</h1>
<div class="pf-c-empty-state__body">
{% trans 'Currently no tokens exist. Click the button below to create one.' %}
</div>
<a href="{% url 'passbook_core:user-tokens-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
</div>
</div>
{% endif %}
</div>

View File

@ -1,11 +1,10 @@
"""passbook user settings template tags"""
from typing import Iterable, List
from typing import Iterable
from django import template
from django.template.context import RequestContext
from passbook.core.models import Source
from passbook.core.types import UIUserSettings
from passbook.flows.models import Stage
from passbook.policies.engine import PolicyEngine
@ -14,26 +13,26 @@ register = template.Library()
@register.simple_tag(takes_context=True)
# 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"""
_all_stages: Iterable[Stage] = Stage.objects.all().select_subclasses()
matching_stages: List[UIUserSettings] = []
matching_stages: list[str] = []
for stage in _all_stages:
user_settings = stage.ui_user_settings
if not user_settings:
continue
matching_stages.append(user_settings)
return sorted(matching_stages, key=lambda x: x.name)
return matching_stages
@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"""
user = context.get("request").user
_all_sources: Iterable[Source] = Source.objects.filter(
enabled=True
).select_subclasses()
matching_sources: List[UIUserSettings] = []
matching_sources: list[str] = []
for source in _all_sources:
user_settings = source.ui_user_settings
if not user_settings:
@ -42,4 +41,4 @@ def user_sources(context: RequestContext) -> List[UIUserSettings]:
policy_engine.build()
if policy_engine.passing:
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
@dataclass
class UIUserSettings:
"""Dataclass for Stage and Source's user_settings"""
name: str
url: str
@dataclass
class UILoginButton:
"""Dataclass for Source's ui_ui_login_button"""
"""Dataclass for Source's ui_login_button"""
# Name, ran through i18n
name: str

View File

@ -10,7 +10,6 @@ from model_utils.managers import InheritanceManager
from rest_framework.serializers import BaseSerializer
from structlog import get_logger
from passbook.core.types import UIUserSettings
from passbook.lib.models import InheritanceForeignKey, SerializerModel
from passbook.policies.models import PolicyBindingModel
@ -64,9 +63,9 @@ class Stage(SerializerModel):
raise NotImplementedError
@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
user settings are available, or an instanace of UIUserSettings."""
user settings are available, or a string with the URL to fetch."""
return None
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 passbook.core.models import Source, UserSourceConnection
from passbook.core.types import UILoginButton, UIUserSettings
from passbook.core.types import UILoginButton
class OAuthSource(Source):
@ -66,12 +66,9 @@ class OAuthSource(Source):
return f"Callback URL: <pre>{url}</pre>"
@property
def ui_user_settings(self) -> Optional[UIUserSettings]:
def ui_user_settings(self) -> Optional[str]:
view_name = "passbook_sources_oauth:oauth-client-user"
return UIUserSettings(
name=self.name,
url=reverse(view_name, kwargs={"source_slug": self.slug}),
)
return reverse(view_name, kwargs={"source_slug": self.slug})
def __str__(self) -> str:
return f"OAuth Source {self.name}"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,3 +8,4 @@ class PassbookStagePasswordConfig(AppConfig):
name = "passbook.stages.password"
label = "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.db import models
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.views import View
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.views import NEXT_ARG_NAME
class PasswordStage(ConfigurableStage, Stage):
@ -51,12 +48,10 @@ class PasswordStage(ConfigurableStage, Stage):
return PasswordStageForm
@property
def ui_user_settings(self) -> Optional[UIUserSettings]:
def ui_user_settings(self) -> Optional[str]:
if not self.configure_flow:
return None
base_url = reverse("passbook_flows:configure", 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}")
return reverse("passbook_stages_password:user-settings", kwargs={"stage_uuid": self.pk})
def __str__(self):
return f"Password Stage {self.name}"

View File

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