diff --git a/authentik/core/templates/if/flow.html b/authentik/core/templates/if/flow.html index 433420175..742a24867 100644 --- a/authentik/core/templates/if/flow.html +++ b/authentik/core/templates/if/flow.html @@ -8,6 +8,12 @@ {% if flow.compatibility_mode and not inspector %} {% endif %} + {% endblock %} {% block head %} diff --git a/authentik/flows/api/flows.py b/authentik/flows/api/flows.py index 3f24679b7..e8babed02 100644 --- a/authentik/flows/api/flows.py +++ b/authentik/flows/api/flows.py @@ -72,6 +72,7 @@ class FlowSerializer(ModelSerializer): "policy_engine_mode", "compatibility_mode", "export_url", + "layout", ] extra_kwargs = { "background": {"read_only": True}, diff --git a/authentik/flows/challenge.py b/authentik/flows/challenge.py index b4766f44c..e3fefd4ca 100644 --- a/authentik/flows/challenge.py +++ b/authentik/flows/challenge.py @@ -2,6 +2,7 @@ from enum import Enum from typing import TYPE_CHECKING, Optional +from django.db import models from django.http import JsonResponse from rest_framework.fields import ChoiceField, DictField from rest_framework.serializers import CharField @@ -17,6 +18,16 @@ PLAN_CONTEXT_URL = "url" PLAN_CONTEXT_ATTRS = "attrs" +class FlowLayout(models.TextChoices): + """Flow layouts""" + + STACKED = "stacked" + CONTENT_LEFT = "content_left" + CONTENT_RIGHT = "content_right" + SIDEBAR_LEFT = "sidebar_left" + SIDEBAR_RIGHT = "sidebar_right" + + class ChallengeTypes(Enum): """Currently defined challenge types""" @@ -38,6 +49,7 @@ class ContextualFlowInfo(PassiveSerializer): title = CharField(required=False, allow_blank=True) background = CharField(required=False) cancel_url = CharField() + layout = ChoiceField(choices=[(x.value, x.name) for x in FlowLayout]) class Challenge(PassiveSerializer): diff --git a/authentik/flows/migrations/0022_flow_layout.py b/authentik/flows/migrations/0022_flow_layout.py new file mode 100644 index 000000000..580b38f36 --- /dev/null +++ b/authentik/flows/migrations/0022_flow_layout.py @@ -0,0 +1,27 @@ +# Generated by Django 4.0.4 on 2022-05-15 19:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("authentik_flows", "0021_auto_20211227_2103"), + ] + + operations = [ + migrations.AddField( + model_name="flow", + name="layout", + field=models.TextField( + choices=[ + ("stacked", "Stacked"), + ("content_left", "Content Left"), + ("content_right", "Content Right"), + ("sidebar_left", "Sidebar Left"), + ("sidebar_right", "Sidebar Right"), + ], + default="stacked", + ), + ), + ] diff --git a/authentik/flows/models.py b/authentik/flows/models.py index f2d436ee4..45f89d4d2 100644 --- a/authentik/flows/models.py +++ b/authentik/flows/models.py @@ -13,6 +13,7 @@ from structlog.stdlib import get_logger from authentik.core.models import Token from authentik.core.types import UserSettingSerializer +from authentik.flows.challenge import FlowLayout from authentik.lib.models import InheritanceForeignKey, SerializerModel from authentik.policies.models import PolicyBindingModel @@ -107,6 +108,7 @@ class Flow(SerializerModel, PolicyBindingModel): slug = models.SlugField(unique=True, help_text=_("Visible in the URL.")) title = models.TextField(help_text=_("Shown as the Title in Flow pages.")) + layout = models.TextField(default=FlowLayout.STACKED, choices=FlowLayout.choices) designation = models.CharField( max_length=100, diff --git a/authentik/flows/stage.py b/authentik/flows/stage.py index e1e5d047b..06dc8e194 100644 --- a/authentik/flows/stage.py +++ b/authentik/flows/stage.py @@ -144,6 +144,7 @@ class ChallengeStageView(StageView): "title": self.format_title(), "background": self.executor.flow.background_url, "cancel_url": reverse("authentik_flows:cancel"), + "layout": self.executor.flow.layout, } ) flow_info.is_valid() diff --git a/authentik/flows/tests/test_inspector.py b/authentik/flows/tests/test_inspector.py index 94804cef3..8d31e6e32 100644 --- a/authentik/flows/tests/test_inspector.py +++ b/authentik/flows/tests/test_inspector.py @@ -55,6 +55,7 @@ class TestFlowInspector(APITestCase): "background": flow.background_url, "cancel_url": reverse("authentik_flows:cancel"), "title": "", + "layout": "stacked", }, "type": ChallengeTypes.NATIVE.value, "password_fields": False, diff --git a/authentik/stages/authenticator_sms/tests.py b/authentik/stages/authenticator_sms/tests.py index 5b7fe17ad..afa6c829a 100644 --- a/authentik/stages/authenticator_sms/tests.py +++ b/authentik/stages/authenticator_sms/tests.py @@ -46,6 +46,7 @@ class AuthenticatorSMSStageTests(APITestCase): "background": self.flow.background_url, "cancel_url": reverse("authentik_flows:cancel"), "title": "", + "layout": "stacked", }, "pending_user": "foo", "pending_user_avatar": "/static/dist/assets/images/user_default.png", diff --git a/schema.yml b/schema.yml index 34da56550..57fc6292d 100644 --- a/schema.yml +++ b/schema.yml @@ -20511,8 +20511,11 @@ components: type: string cancel_url: type: string + layout: + $ref: '#/components/schemas/LayoutEnum' required: - cancel_url + - layout Coordinate: type: object description: Coordinates for diagrams @@ -21473,6 +21476,8 @@ components: export_url: type: string readOnly: true + layout: + $ref: '#/components/schemas/LayoutEnum' required: - background - cache_count @@ -21608,6 +21613,8 @@ components: type: boolean description: Enable compatibility mode, increases compatibility with password managers on mobile devices. + layout: + $ref: '#/components/schemas/LayoutEnum' required: - designation - name @@ -22815,6 +22822,14 @@ components: - name - server_uri - slug + LayoutEnum: + enum: + - stacked + - content_left + - content_right + - sidebar_left + - sidebar_right + type: string Link: type: object description: Returns a single link @@ -27108,6 +27123,8 @@ components: type: boolean description: Enable compatibility mode, increases compatibility with password managers on mobile devices. + layout: + $ref: '#/components/schemas/LayoutEnum' PatchedFlowStageBindingRequest: type: object description: FlowStageBinding Serializer diff --git a/web/src/assets/images/flow_background.jpg b/web/src/assets/images/flow_background.jpg index 18d072e47..2e5d88acd 100644 Binary files a/web/src/assets/images/flow_background.jpg and b/web/src/assets/images/flow_background.jpg differ diff --git a/web/src/flows/FlowExecutor.ts b/web/src/flows/FlowExecutor.ts index 41a067312..39e5ff76c 100644 --- a/web/src/flows/FlowExecutor.ts +++ b/web/src/flows/FlowExecutor.ts @@ -20,6 +20,7 @@ import { CurrentTenant, FlowChallengeResponseRequest, FlowsApi, + LayoutEnum, RedirectChallenge, ShellChallenge, } from "@goauthentik/api"; @@ -38,6 +39,14 @@ import "./stages/captcha/CaptchaStage"; import "./stages/identification/IdentificationStage"; import "./stages/password/PasswordStage"; +export interface FlowWindow extends Window { + authentik: { + flow: { + layout: LayoutEnum; + }; + }; +} + @customElement("ak-flow-executor") export class FlowExecutor extends LitElement implements StageHost { flowSlug?: string; @@ -100,6 +109,44 @@ export class FlowExecutor extends LitElement implements StageHost { .pf-c-drawer__content { background-color: transparent; } + /* layouts */ + .pf-c-login__container.content-right { + grid-template-areas: + "header main" + "footer main" + ". main"; + } + .pf-c-login.sidebar_left { + justify-content: flex-start; + padding-top: 0; + padding-bottom: 0; + } + .pf-c-login.sidebar_left .ak-login-container, + .pf-c-login.sidebar_right .ak-login-container { + height: 100vh; + background-color: var(--pf-c-login__main--BackgroundColor); + padding-left: var(--pf-global--spacer--lg); + padding-right: var(--pf-global--spacer--lg); + } + .pf-c-login.sidebar_left .pf-c-list, + .pf-c-login.sidebar_right .pf-c-list { + color: #000; + } + @media (prefers-color-scheme: dark) { + .pf-c-login.sidebar_left .ak-login-container, + .pf-c-login.sidebar_right .ak-login-container { + background-color: var(--ak-dark-background); + } + .pf-c-login.sidebar_left .pf-c-list, + .pf-c-login.sidebar_right .pf-c-list { + color: var(--ak-dark-foreground); + } + } + .pf-c-login.sidebar_right { + justify-content: flex-end; + padding-top: 0; + padding-bottom: 0; + } `); } @@ -372,6 +419,27 @@ export class FlowExecutor extends LitElement implements StageHost { >`; } + getLayout(): string { + const prefilledFlow = (window as unknown as FlowWindow).authentik.flow.layout; + if (this.challenge) { + return this.challenge?.flowInfo?.layout || prefilledFlow; + } + return prefilledFlow; + } + + getLayoutClass(): string { + const layout = this.getLayout(); + switch (layout) { + case LayoutEnum.ContentLeft: + return "pf-c-login__container"; + case LayoutEnum.ContentRight: + return "pf-c-login__container content-right"; + case LayoutEnum.Stacked: + default: + return "ak-login-container"; + } + } + render(): TemplateResult { return html`
-