web: migrate remaining list views to web

This commit is contained in:
Jens Langhammer 2021-02-20 00:09:53 +01:00
parent 9d4c22c706
commit 854d94056e
17 changed files with 606 additions and 397 deletions

View File

@ -1,109 +0,0 @@
{% extends "administration/base.html" %}
{% load i18n %}
{% load authentik_utils %}
{% block content %}
<section class="pf-c-page__main-section pf-m-light">
<div class="pf-c-content">
<h1>
<i class="pf-icon pf-icon-migration"></i>
{% trans 'Invitations' %}
</h1>
<p>{% trans "Create Invitation Links to enroll Users, and optionally force specific attributes of their account." %}
</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">
<ak-modal-button href="{% url 'authentik_admin:stage-invitation-create' %}">
<ak-spinner-button slot="trigger" class="pf-m-primary">
{% trans 'Create' %}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
<button role="ak-refresh" class="pf-c-button pf-m-primary">
{% trans 'Refresh' %}
</button>
</div>
{% include 'partials/pagination.html' %}
</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 'ID' %}</th>
<th role="columnheader" scope="col">{% trans 'Created by' %}</th>
<th role="columnheader" scope="col">{% trans 'Expiry' %}</th>
<th role="cell"></th>
</tr>
</thead>
<tbody role="rowgroup">
{% for invitation in object_list %}
<tr role="row">
<td role="cell">
<span>
{{ invitation.invite_uuid }}
</span>
</td>
<td role="cell">
<span>
{{ invitation.created_by }}
</span>
</td>
<td role="cell">
<span>
{{ invitation.expiry|default:"-" }}
</span>
</td>
<td>
<ak-modal-button href="{% url 'authentik_admin:stage-invitation-delete' pk=invitation.pk %}">
<ak-spinner-button slot="trigger" class="pf-m-danger">
{% trans 'Delete' %}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="pf-c-pagination pf-m-bottom">
{% include 'partials/pagination.html' %}
</div>
{% else %}
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
{% include 'partials/toolbar_search.html' %}
</div>
</div>
<div class="pf-c-empty-state">
<div class="pf-c-empty-state__content">
<i class="pf-icon pf-icon-migration pf-c-empty-state__icon" aria-hidden="true"></i>
<h1 class="pf-c-title pf-m-lg">
{% trans 'No Invitations.' %}
</h1>
<div class="pf-c-empty-state__body">
{% if request.GET.search != "" %}
{% trans "Your search query doesn't match any invitations." %}
{% else %}
{% trans 'Currently no invitations exist. Click the button below to create one.' %}
{% endif %}
</div>
<ak-modal-button href="{% url 'authentik_admin:stage-invitation-create' %}">
<ak-spinner-button slot="trigger" class="pf-m-primary">
{% trans 'Create' %}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
</div>
</div>
{% endif %}
</div>
</section>
{% endblock %}

View File

@ -1,125 +0,0 @@
{% extends "administration/base.html" %}
{% load i18n %}
{% load authentik_utils %}
{% block content %}
<section class="pf-c-page__main-section pf-m-light">
<div class="pf-c-content">
<h1>
<i class="pf-icon pf-icon-plugged"></i>
{% trans 'Prompts' %}
</h1>
<p>{% trans "Single Prompts that can be used for Prompt Stages." %}</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">
<ak-modal-button href="{% url 'authentik_admin:stage-prompt-create' %}">
<ak-spinner-button slot="trigger" class="pf-m-primary">
{% trans 'Create' %}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
<button role="ak-refresh" class="pf-c-button pf-m-primary">
{% trans 'Refresh' %}
</button>
</div>
{% include 'partials/pagination.html' %}
</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 'Field' %}</th>
<th role="columnheader" scope="col">{% trans 'Label' %}</th>
<th role="columnheader" scope="col">{% trans 'Type' %}</th>
<th role="columnheader" scope="col">{% trans 'Order' %}</th>
<th role="columnheader" scope="col">{% trans 'Flows' %}</th>
<th role="cell"></th>
</tr>
</thead>
<tbody role="rowgroup">
{% for prompt in object_list %}
<tr role="row">
<th role="columnheader">
<div>
<div>{{ prompt.field_key }}</div>
</div>
</th>
<td role="cell">
<div>
{{ prompt.label }}
</div>
</td>
<td role="cell">
<div>
{{ prompt.type }}
</div>
</td>
<td role="cell">
<div>
{{ prompt.order }}
</div>
</td>
<td role="cell">
<ul>
{% for flow in prompt.flow_set.all %}
<li>{{ flow.slug }}</li>
{% empty %}
<li>-</li>
{% endfor %}
</ul>
</td>
<td>
<ak-modal-button href="{% url 'authentik_admin:stage-prompt-update' pk=prompt.pk %}">
<ak-spinner-button slot="trigger" class="pf-m-secondary">
{% trans 'Update' %}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
<ak-modal-button href="{% url 'authentik_admin:stage-prompt-delete' pk=prompt.pk %}">
<ak-spinner-button slot="trigger" class="pf-m-danger">
{% trans 'Delete' %}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="pf-c-pagination pf-m-bottom">
{% include 'partials/pagination.html' %}
</div>
{% else %}
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
{% include 'partials/toolbar_search.html' %}
</div>
</div>
<div class="pf-c-empty-state">
<div class="pf-c-empty-state__content">
<i class="pf-icon pf-icon-plugged pf-c-empty-state__icon" aria-hidden="true"></i>
<h1 class="pf-c-title pf-m-lg">
{% trans 'No Stage Prompts.' %}
</h1>
<div class="pf-c-empty-state__body">
{% if request.GET.search != "" %}
{% trans "Your search query doesn't match any stage prompts." %}
{% else %}
{% trans 'Currently no stage prompts exist. Click the button below to create one.' %}
{% endif %}
</div>
<a href="{% url 'authentik_admin:stage-prompt-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
</div>
</div>
{% endif %}
</div>
</section>
{% endblock %}

View File

@ -153,11 +153,6 @@ urlpatterns = [
name="stage-binding-delete", name="stage-binding-delete",
), ),
# Stage Prompts # Stage Prompts
path(
"stages_prompts/",
stages_prompts.PromptListView.as_view(),
name="stage-prompts",
),
path( path(
"stages_prompts/create/", "stages_prompts/create/",
stages_prompts.PromptCreateView.as_view(), stages_prompts.PromptCreateView.as_view(),
@ -174,11 +169,6 @@ urlpatterns = [
name="stage-prompt-delete", name="stage-prompt-delete",
), ),
# Stage Invitations # Stage Invitations
path(
"stages/invitations/",
stages_invitations.InvitationListView.as_view(),
name="stage-invitations",
),
path( path(
"stages/invitations/create/", "stages/invitations/create/",
stages_invitations.InvitationCreateView.as_view(), stages_invitations.InvitationCreateView.as_view(),

View File

@ -5,37 +5,15 @@ from django.contrib.auth.mixins import (
) )
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.urls import reverse_lazy
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django.views.generic import ListView from guardian.mixins import PermissionRequiredMixin
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from authentik.admin.views.utils import ( from authentik.admin.views.utils import DeleteMessageView
DeleteMessageView,
SearchListMixin,
UserPaginateListMixin,
)
from authentik.lib.views import CreateAssignPermView from authentik.lib.views import CreateAssignPermView
from authentik.stages.invitation.forms import InvitationForm from authentik.stages.invitation.forms import InvitationForm
from authentik.stages.invitation.models import Invitation from authentik.stages.invitation.models import Invitation
class InvitationListView(
LoginRequiredMixin,
PermissionListMixin,
UserPaginateListMixin,
SearchListMixin,
ListView,
):
"""Show list of all invitations"""
model = Invitation
permission_required = "authentik_stages_invitation.view_invitation"
template_name = "administration/stage_invitation/list.html"
ordering = "-expires"
search_fields = ["created_by__username", "expires", "fixed_data"]
class InvitationCreateView( class InvitationCreateView(
SuccessMessageMixin, SuccessMessageMixin,
LoginRequiredMixin, LoginRequiredMixin,
@ -49,7 +27,7 @@ class InvitationCreateView(
permission_required = "authentik_stages_invitation.add_invitation" permission_required = "authentik_stages_invitation.add_invitation"
template_name = "generic/create.html" template_name = "generic/create.html"
success_url = reverse_lazy("authentik_admin:stage-invitations") success_url = "/"
success_message = _("Successfully created Invitation") success_message = _("Successfully created Invitation")
def form_valid(self, form): def form_valid(self, form):
@ -68,5 +46,5 @@ class InvitationDeleteView(
permission_required = "authentik_stages_invitation.delete_invitation" permission_required = "authentik_stages_invitation.delete_invitation"
template_name = "generic/delete.html" template_name = "generic/delete.html"
success_url = reverse_lazy("authentik_admin:stage-invitations") success_url = "/"
success_message = _("Successfully deleted Invitation") success_message = _("Successfully deleted Invitation")

View File

@ -4,42 +4,16 @@ from django.contrib.auth.mixins import (
PermissionRequiredMixin as DjangoPermissionRequiredMixin, PermissionRequiredMixin as DjangoPermissionRequiredMixin,
) )
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.urls import reverse_lazy
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django.views.generic import ListView, UpdateView from django.views.generic import UpdateView
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin from guardian.mixins import PermissionRequiredMixin
from authentik.admin.views.utils import ( from authentik.admin.views.utils import DeleteMessageView
DeleteMessageView,
SearchListMixin,
UserPaginateListMixin,
)
from authentik.lib.views import CreateAssignPermView from authentik.lib.views import CreateAssignPermView
from authentik.stages.prompt.forms import PromptAdminForm from authentik.stages.prompt.forms import PromptAdminForm
from authentik.stages.prompt.models import Prompt from authentik.stages.prompt.models import Prompt
class PromptListView(
LoginRequiredMixin,
PermissionListMixin,
UserPaginateListMixin,
SearchListMixin,
ListView,
):
"""Show list of all prompts"""
model = Prompt
permission_required = "authentik_stages_prompt.view_prompt"
ordering = "order"
template_name = "administration/stage_prompt/list.html"
search_fields = [
"field_key",
"label",
"type",
"placeholder",
]
class PromptCreateView( class PromptCreateView(
SuccessMessageMixin, SuccessMessageMixin,
LoginRequiredMixin, LoginRequiredMixin,
@ -53,7 +27,7 @@ class PromptCreateView(
permission_required = "authentik_stages_prompt.add_prompt" permission_required = "authentik_stages_prompt.add_prompt"
template_name = "generic/create.html" template_name = "generic/create.html"
success_url = reverse_lazy("authentik_admin:stage-prompts") success_url = "/"
success_message = _("Successfully created Prompt") success_message = _("Successfully created Prompt")
@ -70,7 +44,7 @@ class PromptUpdateView(
permission_required = "authentik_stages_prompt.change_prompt" permission_required = "authentik_stages_prompt.change_prompt"
template_name = "generic/update.html" template_name = "generic/update.html"
success_url = reverse_lazy("authentik_admin:stage-prompts") success_url = "/"
success_message = _("Successfully updated Prompt") success_message = _("Successfully updated Prompt")
@ -81,5 +55,5 @@ class PromptDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessag
permission_required = "authentik_stages_prompt.delete_prompt" permission_required = "authentik_stages_prompt.delete_prompt"
template_name = "generic/delete.html" template_name = "generic/delete.html"
success_url = reverse_lazy("authentik_admin:stage-prompts") success_url = "/"
success_message = _("Successfully deleted Prompt") success_message = _("Successfully deleted Prompt")

View File

@ -136,8 +136,8 @@ router.register("stages/captcha", CaptchaStageViewSet)
router.register("stages/consent", ConsentStageViewSet) router.register("stages/consent", ConsentStageViewSet)
router.register("stages/email", EmailStageViewSet) router.register("stages/email", EmailStageViewSet)
router.register("stages/identification", IdentificationStageViewSet) router.register("stages/identification", IdentificationStageViewSet)
router.register("stages/invitation", InvitationStageViewSet)
router.register("stages/invitation/invitations", InvitationViewSet) router.register("stages/invitation/invitations", InvitationViewSet)
router.register("stages/invitation/stages", InvitationStageViewSet)
router.register("stages/password", PasswordStageViewSet) router.register("stages/password", PasswordStageViewSet)
router.register("stages/prompt/prompts", PromptViewSet) router.register("stages/prompt/prompts", PromptViewSet)
router.register("stages/prompt/stages", PromptStageViewSet) router.register("stages/prompt/stages", PromptStageViewSet)

View File

@ -23,6 +23,7 @@ def get_attrs(obj: SerializerModel) -> dict[str, Any]:
"verbose_name_plural", "verbose_name_plural",
"object_type", "object_type",
"flow_set", "flow_set",
"promptstage_set",
) )
for to_remove_name in to_remove: for to_remove_name in to_remove:
if to_remove_name in data: if to_remove_name in data:

View File

@ -34,7 +34,9 @@ class InvitationSerializer(ModelSerializer):
"pk", "pk",
"expires", "expires",
"fixed_data", "fixed_data",
"created_by",
] ]
depth = 2
class InvitationViewSet(ModelViewSet): class InvitationViewSet(ModelViewSet):
@ -42,6 +44,9 @@ class InvitationViewSet(ModelViewSet):
queryset = Invitation.objects.all() queryset = Invitation.objects.all()
serializer_class = InvitationSerializer serializer_class = InvitationSerializer
order = ["-expires"]
search_fields = ["created_by__username", "expires"]
filterset_fields = ["created_by__username", "expires"]
def perform_create(self, serializer: InvitationSerializer): def perform_create(self, serializer: InvitationSerializer):
serializer.instance.created_by = self.request.user serializer.instance.created_by = self.request.user

View File

@ -31,6 +31,8 @@ class PromptStageViewSet(ModelViewSet):
class PromptSerializer(ModelSerializer): class PromptSerializer(ModelSerializer):
"""Prompt Serializer""" """Prompt Serializer"""
promptstage_set = StageSerializer(many=True, required=False)
class Meta: class Meta:
model = Prompt model = Prompt
@ -42,11 +44,13 @@ class PromptSerializer(ModelSerializer):
"required", "required",
"placeholder", "placeholder",
"order", "order",
"promptstage_set",
] ]
class PromptViewSet(ModelViewSet): class PromptViewSet(ModelViewSet):
"""Prompt Viewset""" """Prompt Viewset"""
queryset = Prompt.objects.all() queryset = Prompt.objects.all().prefetch_related("promptstage_set")
serializer_class = PromptSerializer serializer_class = PromptSerializer
filterset_fields = ["field_key", "label", "type", "placeholder"]

View File

@ -6830,78 +6830,21 @@ paths:
required: true required: true
type: string type: string
format: uuid format: uuid
/stages/invitation/:
get:
operationId: stages_invitation_list
description: InvitationStage Viewset
parameters:
- name: ordering
in: query
description: Which field to use when ordering the results.
required: false
type: string
- name: search
in: query
description: A search term.
required: false
type: string
- name: page
in: query
description: A page number within the paginated result set.
required: false
type: integer
- name: page_size
in: query
description: Number of results to return per page.
required: false
type: integer
responses:
'200':
description: ''
schema:
required:
- count
- results
type: object
properties:
count:
type: integer
next:
type: string
format: uri
x-nullable: true
previous:
type: string
format: uri
x-nullable: true
results:
type: array
items:
$ref: '#/definitions/InvitationStage'
tags:
- stages
post:
operationId: stages_invitation_create
description: InvitationStage Viewset
parameters:
- name: data
in: body
required: true
schema:
$ref: '#/definitions/InvitationStage'
responses:
'201':
description: ''
schema:
$ref: '#/definitions/InvitationStage'
tags:
- stages
parameters: []
/stages/invitation/invitations/: /stages/invitation/invitations/:
get: get:
operationId: stages_invitation_invitations_list operationId: stages_invitation_invitations_list
description: Invitation Viewset description: Invitation Viewset
parameters: parameters:
- name: created_by__username
in: query
description: ''
required: false
type: string
- name: expires
in: query
description: ''
required: false
type: string
- name: ordering - name: ordering
in: query in: query
description: Which field to use when ordering the results. description: Which field to use when ordering the results.
@ -7024,9 +6967,76 @@ paths:
required: true required: true
type: string type: string
format: uuid format: uuid
/stages/invitation/{stage_uuid}/: /stages/invitation/stages/:
get: get:
operationId: stages_invitation_read operationId: stages_invitation_stages_list
description: InvitationStage Viewset
parameters:
- name: ordering
in: query
description: Which field to use when ordering the results.
required: false
type: string
- name: search
in: query
description: A search term.
required: false
type: string
- name: page
in: query
description: A page number within the paginated result set.
required: false
type: integer
- name: page_size
in: query
description: Number of results to return per page.
required: false
type: integer
responses:
'200':
description: ''
schema:
required:
- count
- results
type: object
properties:
count:
type: integer
next:
type: string
format: uri
x-nullable: true
previous:
type: string
format: uri
x-nullable: true
results:
type: array
items:
$ref: '#/definitions/InvitationStage'
tags:
- stages
post:
operationId: stages_invitation_stages_create
description: InvitationStage Viewset
parameters:
- name: data
in: body
required: true
schema:
$ref: '#/definitions/InvitationStage'
responses:
'201':
description: ''
schema:
$ref: '#/definitions/InvitationStage'
tags:
- stages
parameters: []
/stages/invitation/stages/{stage_uuid}/:
get:
operationId: stages_invitation_stages_read
description: InvitationStage Viewset description: InvitationStage Viewset
parameters: [] parameters: []
responses: responses:
@ -7037,7 +7047,7 @@ paths:
tags: tags:
- stages - stages
put: put:
operationId: stages_invitation_update operationId: stages_invitation_stages_update
description: InvitationStage Viewset description: InvitationStage Viewset
parameters: parameters:
- name: data - name: data
@ -7053,7 +7063,7 @@ paths:
tags: tags:
- stages - stages
patch: patch:
operationId: stages_invitation_partial_update operationId: stages_invitation_stages_partial_update
description: InvitationStage Viewset description: InvitationStage Viewset
parameters: parameters:
- name: data - name: data
@ -7069,7 +7079,7 @@ paths:
tags: tags:
- stages - stages
delete: delete:
operationId: stages_invitation_delete operationId: stages_invitation_stages_delete
description: InvitationStage Viewset description: InvitationStage Viewset
parameters: [] parameters: []
responses: responses:
@ -7216,6 +7226,26 @@ paths:
operationId: stages_prompt_prompts_list operationId: stages_prompt_prompts_list
description: Prompt Viewset description: Prompt Viewset
parameters: parameters:
- name: field_key
in: query
description: ''
required: false
type: string
- name: label
in: query
description: ''
required: false
type: string
- name: type
in: query
description: ''
required: false
type: string
- name: placeholder
in: query
description: ''
required: false
type: string
- name: ordering - name: ordering
in: query in: query
description: Which field to use when ordering the results. description: Which field to use when ordering the results.
@ -11335,6 +11365,263 @@ definitions:
type: string type: string
format: uuid format: uuid
x-nullable: true x-nullable: true
Invitation:
description: Invitation Serializer
type: object
properties:
pk:
title: Invite uuid
type: string
format: uuid
readOnly: true
expires:
title: Expires
type: string
format: date-time
x-nullable: true
fixed_data:
title: Fixed data
description: Optional fixed data to enforce on user enrollment.
type: object
created_by:
description: Custom User model to allow easier adding o f user-based settings
required:
- password
- username
- name
type: object
properties:
id:
title: ID
type: integer
readOnly: true
password:
title: Password
type: string
maxLength: 128
minLength: 1
last_login:
title: Last login
type: string
format: date-time
x-nullable: true
username:
title: Username
description: Required. 150 characters or fewer. Letters, digits and @/./+/-/_
only.
type: string
pattern: ^[\w.@+-]+$
maxLength: 150
minLength: 1
first_name:
title: First name
type: string
maxLength: 150
last_name:
title: Last name
type: string
maxLength: 150
email:
title: Email address
type: string
format: email
maxLength: 254
is_active:
title: Active
description: Designates whether this user should be treated as active.
Unselect this instead of deleting accounts.
type: boolean
date_joined:
title: Date joined
type: string
format: date-time
uuid:
title: Uuid
type: string
format: uuid
readOnly: true
name:
title: Name
description: User's display name.
type: string
minLength: 1
password_change_date:
title: Password change date
type: string
format: date-time
readOnly: true
attributes:
title: Attributes
type: object
groups:
description: ''
type: array
items:
description: Groups are a generic way of categorizing users to apply
permissions, or some other label, to those users. A user can belong
to any number of groups. A user in a group automatically has all the
permissions granted to that group. For example, if the group 'Site
editors' has the permission can_edit_home_page, any user in that group
will have that permission. Beyond permissions, groups are a convenient
way to categorize users to apply some label, or extended functionality,
to them. For example, you could create a group 'Special users', and
you could write code that would do special things to those users --
such as giving them access to a members-only portion of your site,
or sending them members-only email messages.
required:
- name
type: object
properties:
id:
title: ID
type: integer
readOnly: true
name:
title: Name
type: string
maxLength: 150
minLength: 1
permissions:
type: array
items:
type: integer
uniqueItems: true
readOnly: true
user_permissions:
description: ''
type: array
items:
description: "The permissions system provides a way to assign permissions\
\ to specific users and groups of users. The permission system is\
\ used by the Django admin site, but may also be useful in your own\
\ code. The Django admin site uses permissions as follows: - The \"\
add\" permission limits the user's ability to view the \"add\" form\
\ and add an object. - The \"change\" permission limits a user's ability\
\ to view the change list, view the \"change\" form and change an\
\ object. - The \"delete\" permission limits the ability to delete\
\ an object. - The \"view\" permission limits the ability to view\
\ an object. Permissions are set globally per type of object, not\
\ per specific object instance. It is possible to say \"Mary may change\
\ news stories,\" but it's not currently possible to say \"Mary may\
\ change news stories, but only the ones she created herself\" or\
\ \"Mary may only change news stories that have a certain status or\
\ publication date.\" The permissions listed above are automatically\
\ created for each model."
required:
- name
- codename
- content_type
type: object
properties:
id:
title: ID
type: integer
readOnly: true
name:
title: Name
type: string
maxLength: 255
minLength: 1
codename:
title: Codename
type: string
maxLength: 100
minLength: 1
content_type:
title: Content type
type: integer
readOnly: true
sources:
description: ''
type: array
items:
description: Base Authentication source, i.e. an OAuth Provider, SAML
Remote or LDAP Server
required:
- name
- slug
type: object
properties:
pbm_uuid:
title: Pbm uuid
type: string
format: uuid
readOnly: true
name:
title: Name
description: Source's display Name.
type: string
minLength: 1
slug:
title: Slug
description: Internal source name, used in URLs.
type: string
format: slug
pattern: ^[-a-zA-Z0-9_]+$
maxLength: 50
minLength: 1
enabled:
title: Enabled
type: boolean
authentication_flow:
title: Authentication flow
description: Flow to use when authenticating existing users.
type: string
format: uuid
x-nullable: true
enrollment_flow:
title: Enrollment flow
description: Flow to use when enrolling new users.
type: string
format: uuid
x-nullable: true
policies:
type: array
items:
type: string
format: uuid
readOnly: true
uniqueItems: true
property_mappings:
type: array
items:
type: string
format: uuid
uniqueItems: true
readOnly: true
ak_groups:
description: ''
type: array
items:
description: Custom Group model which supports a basic hierarchy
required:
- name
- parent
type: object
properties:
group_uuid:
title: Group uuid
type: string
format: uuid
readOnly: true
name:
title: Name
type: string
maxLength: 80
minLength: 1
is_superuser:
title: Is superuser
description: Users added to this group will be superusers.
type: boolean
attributes:
title: Attributes
type: object
parent:
title: Parent
type: string
format: uuid
readOnly: true
readOnly: true
InvitationStage: InvitationStage:
description: InvitationStage Serializer description: InvitationStage Serializer
required: required:
@ -11373,24 +11660,6 @@ definitions:
no Invitation is given. By default this Stage will cancel the Flow when no Invitation is given. By default this Stage will cancel the Flow when
no invitation is given. no invitation is given.
type: boolean type: boolean
Invitation:
description: Invitation Serializer
type: object
properties:
pk:
title: Invite uuid
type: string
format: uuid
readOnly: true
expires:
title: Expires
type: string
format: date-time
x-nullable: true
fixed_data:
title: Fixed data
description: Optional fixed data to enforce on user enrollment.
type: object
PasswordStage: PasswordStage:
description: PasswordStage Serializer description: PasswordStage Serializer
required: required:
@ -11496,6 +11765,11 @@ definitions:
type: integer type: integer
maximum: 2147483647 maximum: 2147483647
minimum: -2147483648 minimum: -2147483648
promptstage_set:
description: ''
type: array
items:
$ref: '#/definitions/Stage'
PromptStage: PromptStage:
description: PromptStage Serializer description: PromptStage Serializer
required: required:

View File

@ -0,0 +1,27 @@
import { DefaultClient, QueryArguments, AKResponse } from "./Client";
import { EventContext } from "./Events";
import { User } from "./Users";
export class Invitation {
pk: string;
expires: number;
fixed_date: EventContext;
created_by: User;
constructor() {
throw Error();
}
static get(pk: string): Promise<Invitation> {
return DefaultClient.fetch<Invitation>(["stages", "invitation", "invitations", pk]);
}
static list(filter?: QueryArguments): Promise<AKResponse<Invitation>> {
return DefaultClient.fetch<AKResponse<Invitation>>(["stages", "invitation", "invitations"], filter);
}
static adminUrl(rest: string): string {
return `/administration/stages/invitations/${rest}`;
}
}

30
web/src/api/Prompts.ts Normal file
View File

@ -0,0 +1,30 @@
import { DefaultClient, QueryArguments, AKResponse } from "./Client";
import { Stage } from "./Flows";
export class Prompt {
pk: string;
field_key: string;
label: string;
type: string;
required: boolean;
placeholder: string;
order: number;
promptstage_set: Stage[];
constructor() {
throw Error();
}
static get(pk: string): Promise<Prompt> {
return DefaultClient.fetch<Prompt>(["stages", "prompt", "prompts", pk]);
}
static list(filter?: QueryArguments): Promise<AKResponse<Prompt>> {
return DefaultClient.fetch<AKResponse<Prompt>>(["stages", "prompt", "prompts"], filter);
}
static adminUrl(rest: string): string {
return `/administration/stages/prompts/${rest}`;
}
}

View File

@ -29,7 +29,7 @@ export class SidebarItem {
this.condition = async () => true; this.condition = async () => true;
this.activeMatchers = []; this.activeMatchers = [];
if (this.path) { if (this.path) {
this.activeMatchers.push(new RegExp(`^${this.path}`)); this.activeMatchers.push(new RegExp(`^${this.path}$`));
} }
} }

View File

@ -41,8 +41,8 @@ export const SIDEBAR_ITEMS: SidebarItem[] = [
new SidebarItem("Flows").children( new SidebarItem("Flows").children(
new SidebarItem("Flows", "/flow/flows").activeWhen(`^/flow/flows/(?<slug>${SLUG_REGEX})$`), new SidebarItem("Flows", "/flow/flows").activeWhen(`^/flow/flows/(?<slug>${SLUG_REGEX})$`),
new SidebarItem("Stages", "/flow/stages"), new SidebarItem("Stages", "/flow/stages"),
new SidebarItem("Prompts", "/administration/stages_prompts/"), new SidebarItem("Prompts", "/flow/stages/prompts"),
new SidebarItem("Invitations", "/administration/stages/invitations/"), new SidebarItem("Invitations", "/flow/stages/invitations"),
).when((): Promise<boolean> => { ).when((): Promise<boolean> => {
return User.me().then(u => u.is_superuser); return User.me().then(u => u.is_superuser);
}), }),

View File

@ -0,0 +1,72 @@
import { gettext } from "django";
import { customElement, html, property, TemplateResult } from "lit-element";
import { AKResponse } from "../../api/Client";
import { TablePage } from "../../elements/table/TablePage";
import "../../elements/buttons/ModalButton";
import "../../elements/buttons/SpinnerButton";
import { TableColumn } from "../../elements/table/Table";
import { Invitation } from "../../api/Invitations";
@customElement("ak-stage-invitation-list")
export class InvitationListPage extends TablePage<Invitation> {
searchEnabled(): boolean {
return true;
}
pageTitle(): string {
return gettext("Invitations");
}
pageDescription(): string {
return gettext("Create Invitation Links to enroll Users, and optionally force specific attributes of their account.");
}
pageIcon(): string {
return gettext("pf-icon pf-icon-migration");
}
@property()
order = "expires";
apiEndpoint(page: number): Promise<AKResponse<Invitation>> {
return Invitation.list({
ordering: this.order,
page: page,
search: this.search || "",
});
}
columns(): TableColumn[] {
return [
new TableColumn("ID", "pk"),
new TableColumn("Created by", "created_by"),
new TableColumn("Expiry"),
new TableColumn(""),
];
}
row(item: Invitation): TemplateResult[] {
return [
html`${item.pk}`,
html`${item.created_by.username}`,
html`${new Date(item.expires * 1000).toLocaleString()}`,
html`
<ak-modal-button href="${Invitation.adminUrl(`${item.pk}/delete/`)}">
<ak-spinner-button slot="trigger" class="pf-m-danger">
${gettext("Delete")}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>`,
];
}
renderToolbar(): TemplateResult {
return html`
<ak-modal-button href=${Invitation.adminUrl("create/")}>
<ak-spinner-button slot="trigger" class="pf-m-primary">
${gettext("Create")}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
${super.renderToolbar()}
`;
}
}

View File

@ -0,0 +1,84 @@
import { gettext } from "django";
import { customElement, html, property, TemplateResult } from "lit-element";
import { AKResponse } from "../../api/Client";
import { TablePage } from "../../elements/table/TablePage";
import "../../elements/buttons/ModalButton";
import "../../elements/buttons/SpinnerButton";
import { TableColumn } from "../../elements/table/Table";
import { Prompt } from "../../api/Prompts";
@customElement("ak-stage-prompt-list")
export class PromptListPage extends TablePage<Prompt> {
searchEnabled(): boolean {
return true;
}
pageTitle(): string {
return gettext("Prompts");
}
pageDescription(): string {
return gettext("Single Prompts that can be used for Prompt Stages.");
}
pageIcon(): string {
return gettext("pf-icon pf-icon-plugged");
}
@property()
order = "order";
apiEndpoint(page: number): Promise<AKResponse<Prompt>> {
return Prompt.list({
ordering: this.order,
page: page,
search: this.search || "",
});
}
columns(): TableColumn[] {
return [
new TableColumn("Field", "field_key"),
new TableColumn("Label", "label"),
new TableColumn("Type", "type"),
new TableColumn("Order", "order"),
new TableColumn("Stages"),
new TableColumn(""),
];
}
row(item: Prompt): TemplateResult[] {
return [
html`${item.field_key}`,
html`${item.label}`,
html`${item.type}`,
html`${item.order}`,
html`${item.promptstage_set.map((stage) => {
return html`<li>${stage.name}</li>`;
})}`,
html`
<ak-modal-button href="${Prompt.adminUrl(`${item.pk}/update/`)}">
<ak-spinner-button slot="trigger" class="pf-m-secondary">
${gettext("Edit")}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
<ak-modal-button href="${Prompt.adminUrl(`${item.pk}/delete/`)}">
<ak-spinner-button slot="trigger" class="pf-m-danger">
${gettext("Delete")}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>`,
];
}
renderToolbar(): TemplateResult {
return html`
<ak-modal-button href=${Prompt.adminUrl("create/")}>
<ak-spinner-button slot="trigger" class="pf-m-primary">
${gettext("Create")}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
${super.renderToolbar()}
`;
}
}

View File

@ -22,6 +22,8 @@ import "./pages/providers/ProviderViewPage";
import "./pages/sources/SourcesListPage"; import "./pages/sources/SourcesListPage";
import "./pages/sources/SourceViewPage"; import "./pages/sources/SourceViewPage";
import "./pages/stages/StageListPage"; import "./pages/stages/StageListPage";
import "./pages/stages/InvitationListPage";
import "./pages/stages/PromptListPage";
import "./pages/system-tasks/SystemTaskListPage"; import "./pages/system-tasks/SystemTaskListPage";
import "./pages/tokens/TokenListPage"; import "./pages/tokens/TokenListPage";
import "./pages/users/UserListPage"; import "./pages/users/UserListPage";
@ -49,6 +51,8 @@ export const ROUTES: Route[] = [
new Route(new RegExp("^/identity/groups$"), html`<ak-group-list></ak-group-list>`), new Route(new RegExp("^/identity/groups$"), html`<ak-group-list></ak-group-list>`),
new Route(new RegExp("^/identity/users$"), html`<ak-user-list></ak-user-list>`), new Route(new RegExp("^/identity/users$"), html`<ak-user-list></ak-user-list>`),
new Route(new RegExp("^/core/tokens$"), html`<ak-token-list></ak-token-list>`), new Route(new RegExp("^/core/tokens$"), html`<ak-token-list></ak-token-list>`),
new Route(new RegExp("^/flow/stages/invitations$"), html`<ak-stage-invitation-list></ak-stage-invitation-list>`),
new Route(new RegExp("^/flow/stages/prompts$"), html`<ak-stage-prompt-list></ak-stage-prompt-list>`),
new Route(new RegExp("^/flow/stages$"), html`<ak-stage-list></ak-stage-list>`), new Route(new RegExp("^/flow/stages$"), html`<ak-stage-list></ak-stage-list>`),
new Route(new RegExp("^/flow/flows$"), html`<ak-flow-list></ak-flow-list>`), new Route(new RegExp("^/flow/flows$"), html`<ak-flow-list></ak-flow-list>`),
new Route(new RegExp(`^/flow/flows/(?<slug>${SLUG_REGEX})$`)).then((args) => { new Route(new RegExp(`^/flow/flows/(?<slug>${SLUG_REGEX})$`)).then((args) => {