stages/consent: migrate to SPA
This commit is contained in:
parent
a8681ac88f
commit
b9f409d6d9
|
@ -65,6 +65,26 @@ class ShellChallenge(Challenge):
|
||||||
body = CharField()
|
body = CharField()
|
||||||
|
|
||||||
|
|
||||||
|
class WithUserInfoChallenge(Challenge):
|
||||||
|
"""Challenge base which shows some user info"""
|
||||||
|
|
||||||
|
pending_user = CharField()
|
||||||
|
pending_user_avatar = CharField()
|
||||||
|
|
||||||
|
|
||||||
|
class PermissionSerializer(Serializer):
|
||||||
|
"""Permission used for consent"""
|
||||||
|
|
||||||
|
name = CharField()
|
||||||
|
id = CharField()
|
||||||
|
|
||||||
|
def create(self, validated_data: dict) -> Model:
|
||||||
|
return Model()
|
||||||
|
|
||||||
|
def update(self, instance: Model, validated_data: dict) -> Model:
|
||||||
|
return Model()
|
||||||
|
|
||||||
|
|
||||||
class ChallengeResponse(Serializer):
|
class ChallengeResponse(Serializer):
|
||||||
"""Base class for all challenge responses"""
|
"""Base class for all challenge responses"""
|
||||||
|
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
{% extends 'login/form_with_user.html' %}
|
|
||||||
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block beneath_form %}
|
|
||||||
<div class="pf-c-form__group">
|
|
||||||
<p>
|
|
||||||
{% blocktrans with name=context.application.name %}
|
|
||||||
You're about to sign into <strong id="application-name">{{ name }}</strong>.
|
|
||||||
{% endblocktrans %}
|
|
||||||
</p>
|
|
||||||
<p>{% trans "Application requires following permissions" %}</p>
|
|
||||||
<ul class="pf-c-list" id="scopes">
|
|
||||||
{% for scope_name, description in context.scope_descriptions.items %}
|
|
||||||
<li id="scope-{{ scope_name }}">{{ description }}</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
{{ hidden_inputs }}
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
|
@ -9,6 +9,7 @@ from django.http import HttpRequest, HttpResponse
|
||||||
from django.http.response import Http404
|
from django.http.response import Http404
|
||||||
from django.shortcuts import get_object_or_404, redirect
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik.core.models import Application
|
from authentik.core.models import Application
|
||||||
|
@ -48,14 +49,14 @@ from authentik.providers.oauth2.models import (
|
||||||
from authentik.providers.oauth2.views.userinfo import UserInfoView
|
from authentik.providers.oauth2.views.userinfo import UserInfoView
|
||||||
from authentik.stages.consent.models import ConsentMode, ConsentStage
|
from authentik.stages.consent.models import ConsentMode, ConsentStage
|
||||||
from authentik.stages.consent.stage import (
|
from authentik.stages.consent.stage import (
|
||||||
PLAN_CONTEXT_CONSENT_TEMPLATE,
|
PLAN_CONTEXT_CONSENT_HEADER,
|
||||||
|
PLAN_CONTEXT_CONSENT_PERMISSIONS,
|
||||||
ConsentStageView,
|
ConsentStageView,
|
||||||
)
|
)
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
PLAN_CONTEXT_PARAMS = "params"
|
PLAN_CONTEXT_PARAMS = "params"
|
||||||
PLAN_CONTEXT_SCOPE_DESCRIPTIONS = "scope_descriptions"
|
|
||||||
SESSION_NEEDS_LOGIN = "authentik_oauth2_needs_login"
|
SESSION_NEEDS_LOGIN = "authentik_oauth2_needs_login"
|
||||||
|
|
||||||
ALLOWED_PROMPT_PARAMS = {PROMPT_NONE, PROMPT_CONSNET, PROMPT_LOGIN}
|
ALLOWED_PROMPT_PARAMS = {PROMPT_NONE, PROMPT_CONSNET, PROMPT_LOGIN}
|
||||||
|
@ -432,6 +433,7 @@ class AuthorizationFlowInitView(PolicyAccessView):
|
||||||
planner = FlowPlanner(self.provider.authorization_flow)
|
planner = FlowPlanner(self.provider.authorization_flow)
|
||||||
# planner.use_cache = False
|
# planner.use_cache = False
|
||||||
planner.allow_empty_flows = True
|
planner.allow_empty_flows = True
|
||||||
|
scope_descriptions = UserInfoView().get_scope_descriptions(self.params.scope)
|
||||||
plan: FlowPlan = planner.plan(
|
plan: FlowPlan = planner.plan(
|
||||||
self.request,
|
self.request,
|
||||||
{
|
{
|
||||||
|
@ -439,11 +441,12 @@ class AuthorizationFlowInitView(PolicyAccessView):
|
||||||
PLAN_CONTEXT_APPLICATION: self.application,
|
PLAN_CONTEXT_APPLICATION: self.application,
|
||||||
# OAuth2 related params
|
# OAuth2 related params
|
||||||
PLAN_CONTEXT_PARAMS: self.params,
|
PLAN_CONTEXT_PARAMS: self.params,
|
||||||
PLAN_CONTEXT_SCOPE_DESCRIPTIONS: UserInfoView().get_scope_descriptions(
|
|
||||||
self.params.scope
|
|
||||||
),
|
|
||||||
# Consent related params
|
# Consent related params
|
||||||
PLAN_CONTEXT_CONSENT_TEMPLATE: "providers/oauth2/consent.html",
|
PLAN_CONTEXT_CONSENT_HEADER: _(
|
||||||
|
"You're about to sign into %(application)s."
|
||||||
|
)
|
||||||
|
% {"application": self.application.name},
|
||||||
|
PLAN_CONTEXT_CONSENT_PERMISSIONS: scope_descriptions,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
# OpenID clients can specify a `prompt` parameter, and if its set to consent we
|
# OpenID clients can specify a `prompt` parameter, and if its set to consent we
|
||||||
|
|
|
@ -22,14 +22,16 @@ class UserInfoView(View):
|
||||||
"""Create a dictionary with all the requested claims about the End-User.
|
"""Create a dictionary with all the requested claims about the End-User.
|
||||||
See: http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse"""
|
See: http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse"""
|
||||||
|
|
||||||
def get_scope_descriptions(self, scopes: list[str]) -> dict[str, str]:
|
def get_scope_descriptions(self, scopes: list[str]) -> list[dict[str, str]]:
|
||||||
"""Get a list of all Scopes's descriptions"""
|
"""Get a list of all Scopes's descriptions"""
|
||||||
scope_descriptions = {}
|
scope_descriptions = []
|
||||||
for scope in ScopeMapping.objects.filter(scope_name__in=scopes).order_by(
|
for scope in ScopeMapping.objects.filter(scope_name__in=scopes).order_by(
|
||||||
"scope_name"
|
"scope_name"
|
||||||
):
|
):
|
||||||
if scope.description != "":
|
if scope.description != "":
|
||||||
scope_descriptions[scope.scope_name] = scope.description
|
scope_descriptions.append(
|
||||||
|
{"id": scope.scope_name, "name": scope.description}
|
||||||
|
)
|
||||||
# GitHub Compatibility Scopes are handeled differently, since they required custom paths
|
# GitHub Compatibility Scopes are handeled differently, since they required custom paths
|
||||||
# Hence they don't exist as Scope objects
|
# Hence they don't exist as Scope objects
|
||||||
github_scope_map = {
|
github_scope_map = {
|
||||||
|
@ -44,7 +46,9 @@ class UserInfoView(View):
|
||||||
}
|
}
|
||||||
for scope in scopes:
|
for scope in scopes:
|
||||||
if scope in github_scope_map:
|
if scope in github_scope_map:
|
||||||
scope_descriptions[scope] = github_scope_map[scope]
|
scope_descriptions.append(
|
||||||
|
{"id": scope, "name": github_scope_map[scope]}
|
||||||
|
)
|
||||||
return scope_descriptions
|
return scope_descriptions
|
||||||
|
|
||||||
def get_claims(self, token: RefreshToken) -> dict[str, Any]:
|
def get_claims(self, token: RefreshToken) -> dict[str, Any]:
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
{% extends 'login/form_with_user.html' %}
|
|
||||||
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block beneath_form %}
|
|
||||||
<div class="pf-c-form__group">
|
|
||||||
<p>
|
|
||||||
{% blocktrans with name=context.application.name %}
|
|
||||||
You're about to sign into <strong id="application-name">{{ name }}</strong>.
|
|
||||||
{% endblocktrans %}
|
|
||||||
</p>
|
|
||||||
{{ hidden_inputs }}
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
|
@ -4,6 +4,7 @@ from typing import Optional
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
from django.views.decorators.clickjacking import xframe_options_sameorigin
|
from django.views.decorators.clickjacking import xframe_options_sameorigin
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
@ -31,7 +32,10 @@ from authentik.providers.saml.views.flows import (
|
||||||
SESSION_KEY_AUTH_N_REQUEST,
|
SESSION_KEY_AUTH_N_REQUEST,
|
||||||
SAMLFlowFinalView,
|
SAMLFlowFinalView,
|
||||||
)
|
)
|
||||||
from authentik.stages.consent.stage import PLAN_CONTEXT_CONSENT_TEMPLATE
|
from authentik.stages.consent.stage import (
|
||||||
|
PLAN_CONTEXT_CONSENT_HEADER,
|
||||||
|
PLAN_CONTEXT_CONSENT_PERMISSIONS,
|
||||||
|
)
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
@ -68,7 +72,11 @@ class SAMLSSOView(PolicyAccessView):
|
||||||
{
|
{
|
||||||
PLAN_CONTEXT_SSO: True,
|
PLAN_CONTEXT_SSO: True,
|
||||||
PLAN_CONTEXT_APPLICATION: self.application,
|
PLAN_CONTEXT_APPLICATION: self.application,
|
||||||
PLAN_CONTEXT_CONSENT_TEMPLATE: "providers/saml/consent.html",
|
PLAN_CONTEXT_CONSENT_HEADER: _(
|
||||||
|
"You're about to sign into %(application)s."
|
||||||
|
)
|
||||||
|
% {"application": self.application.name},
|
||||||
|
PLAN_CONTEXT_CONSENT_PERMISSIONS: [],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
plan.append(in_memory_stage(SAMLFlowFinalView))
|
plan.append(in_memory_stage(SAMLFlowFinalView))
|
||||||
|
|
|
@ -4,10 +4,6 @@ from django import forms
|
||||||
from authentik.stages.consent.models import ConsentStage
|
from authentik.stages.consent.models import ConsentStage
|
||||||
|
|
||||||
|
|
||||||
class ConsentForm(forms.Form):
|
|
||||||
"""authentik consent stage form"""
|
|
||||||
|
|
||||||
|
|
||||||
class ConsentStageForm(forms.ModelForm):
|
class ConsentStageForm(forms.ModelForm):
|
||||||
"""Form to edit ConsentStage Instance"""
|
"""Form to edit ConsentStage Instance"""
|
||||||
|
|
||||||
|
|
|
@ -1,40 +1,69 @@
|
||||||
"""authentik consent stage"""
|
"""authentik consent stage"""
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.views.generic import FormView
|
from rest_framework.fields import CharField
|
||||||
|
|
||||||
|
from authentik.core.models import User
|
||||||
|
from authentik.flows.challenge import (
|
||||||
|
Challenge,
|
||||||
|
ChallengeResponse,
|
||||||
|
ChallengeTypes,
|
||||||
|
PermissionSerializer,
|
||||||
|
WithUserInfoChallenge,
|
||||||
|
)
|
||||||
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_PENDING_USER
|
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_PENDING_USER
|
||||||
from authentik.flows.stage import StageView
|
from authentik.flows.stage import ChallengeStageView
|
||||||
|
from authentik.lib.templatetags.authentik_utils import avatar
|
||||||
from authentik.lib.utils.time import timedelta_from_string
|
from authentik.lib.utils.time import timedelta_from_string
|
||||||
from authentik.stages.consent.forms import ConsentForm
|
|
||||||
from authentik.stages.consent.models import ConsentMode, ConsentStage, UserConsent
|
from authentik.stages.consent.models import ConsentMode, ConsentStage, UserConsent
|
||||||
|
|
||||||
PLAN_CONTEXT_CONSENT_TEMPLATE = "consent_template"
|
PLAN_CONTEXT_CONSENT_HEADER = "consent_header"
|
||||||
|
PLAN_CONTEXT_CONSENT_PERMISSIONS = "consent_permissions"
|
||||||
|
|
||||||
|
|
||||||
class ConsentStageView(FormView, StageView):
|
class ConsentChallenge(WithUserInfoChallenge):
|
||||||
|
"""Challenge info for consent screens"""
|
||||||
|
|
||||||
|
header_text = CharField()
|
||||||
|
permissions = PermissionSerializer(many=True)
|
||||||
|
|
||||||
|
|
||||||
|
class ConsentChallengeResponse(ChallengeResponse):
|
||||||
|
"""Consent challenge response, any valid response request is valid"""
|
||||||
|
|
||||||
|
|
||||||
|
class ConsentStageView(ChallengeStageView):
|
||||||
"""Simple consent checker."""
|
"""Simple consent checker."""
|
||||||
|
|
||||||
form_class = ConsentForm
|
response_class = ConsentChallengeResponse
|
||||||
|
|
||||||
def get_context_data(self, **kwargs: dict[str, Any]) -> dict[str, Any]:
|
def get_challenge(self) -> Challenge:
|
||||||
kwargs = super().get_context_data(**kwargs)
|
challenge = ConsentChallenge(
|
||||||
kwargs["current_stage"] = self.executor.current_stage
|
data={
|
||||||
kwargs["context"] = self.executor.plan.context
|
"type": ChallengeTypes.native,
|
||||||
return kwargs
|
"component": "ak-stage-consent",
|
||||||
|
}
|
||||||
def get_template_names(self) -> list[str]:
|
)
|
||||||
# PLAN_CONTEXT_CONSENT_TEMPLATE has to be set by a template that calls this stage
|
if PLAN_CONTEXT_CONSENT_HEADER in self.executor.plan.context:
|
||||||
if PLAN_CONTEXT_CONSENT_TEMPLATE in self.executor.plan.context:
|
challenge.initial_data["header_text"] = self.executor.plan.context[
|
||||||
template_name = self.executor.plan.context[PLAN_CONTEXT_CONSENT_TEMPLATE]
|
PLAN_CONTEXT_CONSENT_HEADER
|
||||||
return [template_name]
|
]
|
||||||
return ["stages/consent/fallback.html"]
|
if PLAN_CONTEXT_CONSENT_PERMISSIONS in self.executor.plan.context:
|
||||||
|
challenge.initial_data["permissions"] = self.executor.plan.context[
|
||||||
|
PLAN_CONTEXT_CONSENT_PERMISSIONS
|
||||||
|
]
|
||||||
|
# If there's a pending user, update the `username` field
|
||||||
|
# this field is only used by password managers.
|
||||||
|
# If there's no user set, an error is raised later.
|
||||||
|
if PLAN_CONTEXT_PENDING_USER in self.executor.plan.context:
|
||||||
|
pending_user: User = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER]
|
||||||
|
challenge.initial_data["pending_user"] = pending_user.username
|
||||||
|
challenge.initial_data["pending_user_avatar"] = avatar(pending_user)
|
||||||
|
return challenge
|
||||||
|
|
||||||
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||||
current_stage: ConsentStage = self.executor.current_stage
|
current_stage: ConsentStage = self.executor.current_stage
|
||||||
# For always require, we always show the form
|
# For always require, we always return the challenge
|
||||||
if current_stage.mode == ConsentMode.ALWAYS_REQUIRE:
|
if current_stage.mode == ConsentMode.ALWAYS_REQUIRE:
|
||||||
return super().get(request, *args, **kwargs)
|
return super().get(request, *args, **kwargs)
|
||||||
# at this point we need to check consent from database
|
# at this point we need to check consent from database
|
||||||
|
@ -51,10 +80,10 @@ class ConsentStageView(FormView, StageView):
|
||||||
if UserConsent.filter_not_expired(user=user, application=application).exists():
|
if UserConsent.filter_not_expired(user=user, application=application).exists():
|
||||||
return self.executor.stage_ok()
|
return self.executor.stage_ok()
|
||||||
|
|
||||||
# No consent found, show form
|
# No consent found, return consent
|
||||||
return super().get(request, *args, **kwargs)
|
return super().get(request, *args, **kwargs)
|
||||||
|
|
||||||
def form_valid(self, form: ConsentForm) -> HttpResponse:
|
def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
|
||||||
current_stage: ConsentStage = self.executor.current_stage
|
current_stage: ConsentStage = self.executor.current_stage
|
||||||
if PLAN_CONTEXT_APPLICATION not in self.executor.plan.context:
|
if PLAN_CONTEXT_APPLICATION not in self.executor.plan.context:
|
||||||
return self.executor.stage_ok()
|
return self.executor.stage_ok()
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
{% extends 'login/form_with_user.html' %}
|
|
||||||
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block beneath_form %}
|
|
||||||
<div class="pf-c-form__group">
|
|
||||||
{{ hidden_inputs }}
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
|
@ -4,6 +4,7 @@ from django.urls import reverse
|
||||||
from django.utils.encoding import force_str
|
from django.utils.encoding import force_str
|
||||||
|
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
|
from authentik.flows.challenge import ChallengeTypes
|
||||||
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
|
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||||
from authentik.sources.oauth.models import OAuthSource
|
from authentik.sources.oauth.models import OAuthSource
|
||||||
from authentik.stages.identification.models import IdentificationStage, UserFields
|
from authentik.stages.identification.models import IdentificationStage, UserFields
|
||||||
|
@ -102,7 +103,7 @@ class TestIdentificationStage(TestCase):
|
||||||
self.assertJSONEqual(
|
self.assertJSONEqual(
|
||||||
force_str(response.content),
|
force_str(response.content),
|
||||||
{
|
{
|
||||||
"type": "native",
|
"type": ChallengeTypes.native,
|
||||||
"component": "ak-stage-identification",
|
"component": "ak-stage-identification",
|
||||||
"input_type": "email",
|
"input_type": "email",
|
||||||
"enroll_url": "/flows/unique-enrollment-string/",
|
"enroll_url": "/flows/unique-enrollment-string/",
|
||||||
|
@ -141,7 +142,7 @@ class TestIdentificationStage(TestCase):
|
||||||
self.assertJSONEqual(
|
self.assertJSONEqual(
|
||||||
force_str(response.content),
|
force_str(response.content),
|
||||||
{
|
{
|
||||||
"type": "native",
|
"type": ChallengeTypes.native,
|
||||||
"component": "ak-stage-identification",
|
"component": "ak-stage-identification",
|
||||||
"input_type": "email",
|
"input_type": "email",
|
||||||
"recovery_url": "/flows/unique-recovery-string/",
|
"recovery_url": "/flows/unique-recovery-string/",
|
||||||
|
|
|
@ -10,11 +10,15 @@ from django.urls import reverse
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from rest_framework.exceptions import ErrorDetail
|
from rest_framework.exceptions import ErrorDetail
|
||||||
from rest_framework.fields import CharField
|
from rest_framework.fields import CharField
|
||||||
from rest_framework.serializers import ValidationError
|
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes
|
from authentik.flows.challenge import (
|
||||||
|
Challenge,
|
||||||
|
ChallengeResponse,
|
||||||
|
ChallengeTypes,
|
||||||
|
WithUserInfoChallenge,
|
||||||
|
)
|
||||||
from authentik.flows.models import Flow, FlowDesignation
|
from authentik.flows.models import Flow, FlowDesignation
|
||||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
|
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
|
||||||
from authentik.flows.stage import ChallengeStageView
|
from authentik.flows.stage import ChallengeStageView
|
||||||
|
@ -55,11 +59,9 @@ def authenticate(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class PasswordChallenge(Challenge):
|
class PasswordChallenge(WithUserInfoChallenge):
|
||||||
"""Password challenge UI fields"""
|
"""Password challenge UI fields"""
|
||||||
|
|
||||||
pending_user = CharField()
|
|
||||||
pending_user_avatar = CharField()
|
|
||||||
recovery_url = CharField(required=False)
|
recovery_url = CharField(required=False)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -152,7 +152,7 @@ class TestProviderOAuth2Github(SeleniumTestCase):
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
"GitHub Compatibility: Access you Email addresses",
|
"GitHub Compatibility: Access you Email addresses",
|
||||||
self.driver.find_element(By.ID, "scope-user:email").text,
|
self.driver.find_element(By.ID, "permission-user:email").text,
|
||||||
)
|
)
|
||||||
self.driver.find_element(
|
self.driver.find_element(
|
||||||
By.CSS_SELECTOR,
|
By.CSS_SELECTOR,
|
||||||
|
|
|
@ -23,6 +23,10 @@ export interface Challenge {
|
||||||
title?: string;
|
title?: string;
|
||||||
response_errors?: ErrorDict;
|
response_errors?: ErrorDict;
|
||||||
}
|
}
|
||||||
|
export interface WithUserInfoChallenge extends Challenge {
|
||||||
|
pending_user: string;
|
||||||
|
pending_user_avatar: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ShellChallenge extends Challenge {
|
export interface ShellChallenge extends Challenge {
|
||||||
body: string;
|
body: string;
|
||||||
|
|
|
@ -0,0 +1,77 @@
|
||||||
|
import { gettext } from "django";
|
||||||
|
import { CSSResult, customElement, html, property, TemplateResult } from "lit-element";
|
||||||
|
import { WithUserInfoChallenge } from "../../../api/Flows";
|
||||||
|
import { COMMON_STYLES } from "../../../common/styles";
|
||||||
|
import { BaseStage } from "../base";
|
||||||
|
|
||||||
|
export interface Permission {
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConsentChallenge extends WithUserInfoChallenge {
|
||||||
|
|
||||||
|
header_text: string;
|
||||||
|
permissions?: Permission[];
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement("ak-stage-consent")
|
||||||
|
export class ConsentStage extends BaseStage {
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
challenge?: ConsentChallenge;
|
||||||
|
|
||||||
|
static get styles(): CSSResult[] {
|
||||||
|
return COMMON_STYLES;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(): TemplateResult {
|
||||||
|
if (!this.challenge) {
|
||||||
|
return html`<ak-loading-state></ak-loading-state>`;
|
||||||
|
}
|
||||||
|
return html`<header class="pf-c-login__main-header">
|
||||||
|
<h1 class="pf-c-title pf-m-3xl">
|
||||||
|
${this.challenge.title}
|
||||||
|
</h1>
|
||||||
|
</header>
|
||||||
|
<div class="pf-c-login__main-body">
|
||||||
|
<form class="pf-c-form" @submit=${(e: Event) => { this.submit(e); }}>
|
||||||
|
<div class="pf-c-form__group">
|
||||||
|
<div class="form-control-static">
|
||||||
|
<div class="left">
|
||||||
|
<img class="pf-c-avatar" src="${this.challenge.pending_user_avatar}" alt="${gettext("User's avatar")}">
|
||||||
|
${this.challenge.pending_user}
|
||||||
|
</div>
|
||||||
|
<div class="right">
|
||||||
|
<a href="/-/cancel/">${gettext("Not you?")}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pf-c-form__group">
|
||||||
|
<p>
|
||||||
|
${this.challenge.header_text}
|
||||||
|
</p>
|
||||||
|
<p>${gettext("Application requires following permissions")}</p>
|
||||||
|
<ul class="pf-c-list" id="permmissions">
|
||||||
|
${(this.challenge.permissions || []).map((permission) => {
|
||||||
|
return html`<li id="permission-${permission.id}">${permission.name}</li>`;
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pf-c-form__group pf-m-action">
|
||||||
|
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
|
||||||
|
${gettext("Continue")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<footer class="pf-c-login__main-footer">
|
||||||
|
<ul class="pf-c-login__main-footer-links">
|
||||||
|
</ul>
|
||||||
|
</footer>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,15 +1,11 @@
|
||||||
import { gettext } from "django";
|
import { gettext } from "django";
|
||||||
import { CSSResult, customElement, html, property, TemplateResult } from "lit-element";
|
import { CSSResult, customElement, html, property, TemplateResult } from "lit-element";
|
||||||
import { Challenge } from "../../../api/Flows";
|
import { WithUserInfoChallenge } from "../../../api/Flows";
|
||||||
import { COMMON_STYLES } from "../../../common/styles";
|
import { COMMON_STYLES } from "../../../common/styles";
|
||||||
import { BaseStage } from "../base";
|
import { BaseStage } from "../base";
|
||||||
|
|
||||||
export interface PasswordChallenge extends Challenge {
|
export interface PasswordChallenge extends WithUserInfoChallenge {
|
||||||
|
|
||||||
pending_user: string;
|
|
||||||
pending_user_avatar: string;
|
|
||||||
recovery_url?: string;
|
recovery_url?: string;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@customElement("ak-stage-password")
|
@customElement("ak-stage-password")
|
||||||
|
|
|
@ -4,10 +4,12 @@ import { unsafeHTML } from "lit-html/directives/unsafe-html";
|
||||||
import { getCookie } from "../../utils";
|
import { getCookie } from "../../utils";
|
||||||
import "../../elements/stages/identification/IdentificationStage";
|
import "../../elements/stages/identification/IdentificationStage";
|
||||||
import "../../elements/stages/password/PasswordStage";
|
import "../../elements/stages/password/PasswordStage";
|
||||||
|
import "../../elements/stages/consent/ConsentStage";
|
||||||
import { ShellChallenge, Challenge, ChallengeTypes, Flow, RedirectChallenge } from "../../api/Flows";
|
import { ShellChallenge, Challenge, ChallengeTypes, Flow, RedirectChallenge } from "../../api/Flows";
|
||||||
import { DefaultClient } from "../../api/Client";
|
import { DefaultClient } from "../../api/Client";
|
||||||
import { IdentificationChallenge } from "../../elements/stages/identification/IdentificationStage";
|
import { IdentificationChallenge } from "../../elements/stages/identification/IdentificationStage";
|
||||||
import { PasswordChallenge } from "../../elements/stages/password/PasswordStage";
|
import { PasswordChallenge } from "../../elements/stages/password/PasswordStage";
|
||||||
|
import { ConsentChallenge } from "../../elements/stages/consent/ConsentStage";
|
||||||
|
|
||||||
@customElement("ak-flow-executor")
|
@customElement("ak-flow-executor")
|
||||||
export class FlowExecutor extends LitElement {
|
export class FlowExecutor extends LitElement {
|
||||||
|
@ -108,6 +110,8 @@ export class FlowExecutor extends LitElement {
|
||||||
return html`<ak-stage-identification .host=${this} .challenge=${this.challenge as IdentificationChallenge}></ak-stage-identification>`;
|
return html`<ak-stage-identification .host=${this} .challenge=${this.challenge as IdentificationChallenge}></ak-stage-identification>`;
|
||||||
case "ak-stage-password":
|
case "ak-stage-password":
|
||||||
return html`<ak-stage-password .host=${this} .challenge=${this.challenge as PasswordChallenge}></ak-stage-password>`;
|
return html`<ak-stage-password .host=${this} .challenge=${this.challenge as PasswordChallenge}></ak-stage-password>`;
|
||||||
|
case "ak-stage-consent":
|
||||||
|
return html`<ak-stage-consent .host=${this} .challenge=${this.challenge as ConsentChallenge}></ak-stage-consent>`;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
Reference in New Issue