web: use generated API Client (#616)

* api: fix types for config API

* api: remove broken swagger UI

* admin: re-fix system task enum

* events: make event optional

* events: fix Schema for notification transport test

* flows: use APIView for Flow Executor

* core: fix schema for Metrics APIs

* web: rewrite to use generated API client

* web: generate API Client in CI

* admin: use x_cord and y_cord to prevent yaml issues

* events: fix linting errors

* web: don't lint generated code

* core: fix fields not being required in TypeSerializer

* flows: fix missing permission_classes

* web: cleanup

* web: fix rendering of graph on Overview page

* web: cleanup imports

* core: fix missing background image filter

* flows: fix flows not advancing properly

* stages/*: fix warnings during get_challenge

* web: send Flow response as JSON instead of FormData

* web: fix styles for horizontal tabs

* web: add base chart class and custom chart for application view

* root: generate ts client for e2e tests

* web: don't attempt to connect to websocket in selenium tests

* web: fix UserTokenList not being included in the build

* web: fix styling for static token list

* web: fix CSRF Token missing

* stages/authenticator_static: fix error when disable static tokens

* core: fix display issue when updating user info

* web: fix Flow executor not showing spinner when redirecting
This commit is contained in:
Jens L 2021-03-08 11:14:00 +01:00 committed by GitHub
parent 1c6d498621
commit 2852fa3c5e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
146 changed files with 1593 additions and 1882 deletions

View file

@ -59,6 +59,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v1
- name: prepare ts api client
run: |
docker run --rm -v $(pwd):/local openapitools/openapi-generator-cli generate -i /local/swagger.yaml -g typescript-fetch -o /local/web/src/api --additional-properties=typescriptThreePlus=true
- name: Docker Login Registry - name: Docker Login Registry
env: env:
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}

View file

@ -45,4 +45,5 @@ COPY ./lifecycle/ /lifecycle
USER authentik USER authentik
STOPSIGNAL SIGINT STOPSIGNAL SIGINT
ENV TMPDIR /dev/shm/ ENV TMPDIR /dev/shm/
ENV PYTHONUBUFFERED 1
ENTRYPOINT [ "/lifecycle/bootstrap.sh" ] ENTRYPOINT [ "/lifecycle/bootstrap.sh" ]

View file

@ -7,8 +7,8 @@ from django.db.models import Count, ExpressionWrapper, F, Model
from django.db.models.fields import DurationField from django.db.models.fields import DurationField
from django.db.models.functions import ExtractHour from django.db.models.functions import ExtractHour
from django.utils.timezone import now from django.utils.timezone import now
from drf_yasg2.utils import swagger_auto_schema from drf_yasg2.utils import swagger_auto_schema, swagger_serializer_method
from rest_framework.fields import SerializerMethodField from rest_framework.fields import IntegerField, SerializerMethodField
from rest_framework.permissions import IsAdminUser from rest_framework.permissions import IsAdminUser
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
@ -37,23 +37,39 @@ def get_events_per_1h(**filter_kwargs) -> list[dict[str, int]]:
for hour in range(0, -24, -1): for hour in range(0, -24, -1):
results.append( results.append(
{ {
"x": time.mktime((_now + timedelta(hours=hour)).timetuple()) * 1000, "x_cord": time.mktime((_now + timedelta(hours=hour)).timetuple())
"y": data[hour * -1], * 1000,
"y_cord": data[hour * -1],
} }
) )
return results return results
class AdministrationMetricsSerializer(Serializer): class CoordinateSerializer(Serializer):
"""Coordinates for diagrams"""
x_cord = IntegerField(read_only=True)
y_cord = IntegerField(read_only=True)
def create(self, validated_data: dict) -> Model:
raise NotImplementedError
def update(self, instance: Model, validated_data: dict) -> Model:
raise NotImplementedError
class LoginMetricsSerializer(Serializer):
"""Login Metrics per 1h""" """Login Metrics per 1h"""
logins_per_1h = SerializerMethodField() logins_per_1h = SerializerMethodField()
logins_failed_per_1h = SerializerMethodField() logins_failed_per_1h = SerializerMethodField()
@swagger_serializer_method(serializer_or_field=CoordinateSerializer(many=True))
def get_logins_per_1h(self, _): def get_logins_per_1h(self, _):
"""Get successful logins per hour for the last 24 hours""" """Get successful logins per hour for the last 24 hours"""
return get_events_per_1h(action=EventAction.LOGIN) return get_events_per_1h(action=EventAction.LOGIN)
@swagger_serializer_method(serializer_or_field=CoordinateSerializer(many=True))
def get_logins_failed_per_1h(self, _): def get_logins_failed_per_1h(self, _):
"""Get failed logins per hour for the last 24 hours""" """Get failed logins per hour for the last 24 hours"""
return get_events_per_1h(action=EventAction.LOGIN_FAILED) return get_events_per_1h(action=EventAction.LOGIN_FAILED)
@ -70,8 +86,8 @@ class AdministrationMetricsViewSet(ViewSet):
permission_classes = [IsAdminUser] permission_classes = [IsAdminUser]
@swagger_auto_schema(responses={200: AdministrationMetricsSerializer(many=True)}) @swagger_auto_schema(responses={200: LoginMetricsSerializer(many=False)})
def list(self, request: Request) -> Response: def list(self, request: Request) -> Response:
"""Login Metrics per 1h""" """Login Metrics per 1h"""
serializer = AdministrationMetricsSerializer(True) serializer = LoginMetricsSerializer(True)
return Response(serializer.data) return Response(serializer.data)

View file

@ -25,8 +25,8 @@ class TaskSerializer(Serializer):
task_finish_timestamp = DateTimeField(source="finish_timestamp") task_finish_timestamp = DateTimeField(source="finish_timestamp")
status = ChoiceField( status = ChoiceField(
source="result.status.value", source="result.status.name",
choices=[(x.value, x.name) for x in TaskResultStatus], choices=[(x.name, x.name) for x in TaskResultStatus],
) )
messages = ListField(source="result.messages") messages = ListField(source="result.messages")

View file

@ -1,4 +1,5 @@
"""api v2 urls""" """api v2 urls"""
from django.conf import settings
from django.urls import path, re_path from django.urls import path, re_path
from drf_yasg2 import openapi from drf_yasg2 import openapi
from drf_yasg2.views import get_schema_view from drf_yasg2.views import get_schema_view
@ -156,6 +157,14 @@ router.register("stages/user_write", UserWriteStageViewSet)
router.register("stages/dummy", DummyStageViewSet) router.register("stages/dummy", DummyStageViewSet)
router.register("policies/dummy", DummyPolicyViewSet) router.register("policies/dummy", DummyPolicyViewSet)
api_urls = router.urls + [
path(
"flows/executor/<slug:flow_slug>/",
FlowExecutorView.as_view(),
name="flow-executor",
),
]
info = openapi.Info( info = openapi.Info(
title="authentik API", title="authentik API",
default_version="v2", default_version="v2",
@ -165,26 +174,22 @@ info = openapi.Info(
), ),
) )
SchemaView = get_schema_view( SchemaView = get_schema_view(
info, info, public=True, permission_classes=(AllowAny,), patterns=api_urls
public=True,
permission_classes=(AllowAny,),
) )
urlpatterns = [ urlpatterns = api_urls + [
re_path( re_path(
r"^swagger(?P<format>\.json|\.yaml)$", r"^swagger(?P<format>\.json|\.yaml)$",
SchemaView.without_ui(cache_timeout=0), SchemaView.without_ui(cache_timeout=0),
name="schema-json", name="schema-json",
), ),
path( ]
"swagger/",
SchemaView.with_ui("swagger", cache_timeout=0), if settings.DEBUG:
name="schema-swagger-ui", urlpatterns = urlpatterns + [
), path(
path("redoc/", SchemaView.with_ui("redoc", cache_timeout=0), name="schema-redoc"), "swagger/",
path( SchemaView.with_ui("swagger", cache_timeout=0),
"flows/executor/<slug:flow_slug>/", name="schema-swagger-ui",
FlowExecutorView.as_view(), ),
name="flow-executor", ]
),
] + router.urls

View file

@ -2,6 +2,7 @@
from django.core.cache import cache from django.core.cache import cache
from django.db.models import QuerySet from django.db.models import QuerySet
from django.http.response import Http404 from django.http.response import Http404
from drf_yasg2.utils import swagger_auto_schema
from guardian.shortcuts import get_objects_for_user from guardian.shortcuts import get_objects_for_user
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.fields import SerializerMethodField from rest_framework.fields import SerializerMethodField
@ -13,7 +14,7 @@ from rest_framework.viewsets import ModelViewSet
from rest_framework_guardian.filters import ObjectPermissionsFilter from rest_framework_guardian.filters import ObjectPermissionsFilter
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.admin.api.metrics import get_events_per_1h from authentik.admin.api.metrics import CoordinateSerializer, get_events_per_1h
from authentik.core.api.providers import ProviderSerializer from authentik.core.api.providers import ProviderSerializer
from authentik.core.models import Application from authentik.core.models import Application
from authentik.events.models import EventAction from authentik.events.models import EventAction
@ -109,6 +110,7 @@ class ApplicationViewSet(ModelViewSet):
serializer = self.get_serializer(allowed_applications, many=True) serializer = self.get_serializer(allowed_applications, many=True)
return self.get_paginated_response(serializer.data) return self.get_paginated_response(serializer.data)
@swagger_auto_schema(responses={200: CoordinateSerializer(many=True)})
@action(detail=True) @action(detail=True)
def metrics(self, request: Request, slug: str): def metrics(self, request: Request, slug: str):
"""Metrics for application logins""" """Metrics for application logins"""

View file

@ -28,9 +28,9 @@ class MetaNameSerializer(Serializer):
class TypeCreateSerializer(Serializer): class TypeCreateSerializer(Serializer):
"""Types of an object that can be created""" """Types of an object that can be created"""
name = CharField(read_only=True) name = CharField(required=True)
description = CharField(read_only=True) description = CharField(required=True)
link = CharField(read_only=True) link = CharField(required=True)
def create(self, validated_data: dict) -> Model: def create(self, validated_data: dict) -> Model:
raise NotImplementedError raise NotImplementedError

View file

@ -16,6 +16,16 @@
{% block body %} {% block body %}
<div class="pf-c-background-image"> <div class="pf-c-background-image">
<svg xmlns="http://www.w3.org/2000/svg" class="pf-c-background-image__filter" width="0" height="0">
<filter id="image_overlay">
<feComponentTransfer color-interpolation-filters="sRGB" result="duotone">
<feFuncR type="table" tableValues="0.0086274509803922 0.63921568627451"></feFuncR>
<feFuncG type="table" tableValues="0.0086274509803922 0.63921568627451"></feFuncG>
<feFuncB type="table" tableValues="0.0086274509803922 0.63921568627451"></feFuncB>
<feFuncA type="table" tableValues="0 1"></feFuncA>
</feComponentTransfer>
</filter>
</svg>
</div> </div>
<ak-message-container></ak-message-container> <ak-message-container></ak-message-container>
<div class="pf-c-login"> <div class="pf-c-login">

View file

@ -7,6 +7,7 @@ from django.contrib.auth.mixins import (
) )
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.http.response import HttpResponse from django.http.response import HttpResponse
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 UpdateView from django.views.generic import UpdateView
from django.views.generic.base import TemplateView from django.views.generic.base import TemplateView
@ -34,7 +35,7 @@ class UserDetailsView(SuccessMessageMixin, LoginRequiredMixin, UpdateView):
form_class = UserDetailForm form_class = UserDetailForm
success_message = _("Successfully updated user.") success_message = _("Successfully updated user.")
success_url = "/" success_url = reverse_lazy("authentik_core:user-details")
def get_object(self): def get_object(self):
return self.request.user return self.request.user

View file

@ -13,7 +13,7 @@ class NotificationSerializer(ModelSerializer):
body = ReadOnlyField() body = ReadOnlyField()
severity = ReadOnlyField() severity = ReadOnlyField()
event = EventSerializer() event = EventSerializer(required=False)
class Meta: class Meta:

View file

@ -1,11 +1,12 @@
"""NotificationTransport API Views""" """NotificationTransport API Views"""
from django.http.response import Http404 from django.http.response import Http404
from drf_yasg2.utils import no_body, swagger_auto_schema
from guardian.shortcuts import get_objects_for_user from guardian.shortcuts import get_objects_for_user
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.fields import SerializerMethodField from rest_framework.fields import CharField, ListField, SerializerMethodField
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer from rest_framework.serializers import ModelSerializer, Serializer
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.events.models import ( from authentik.events.models import (
@ -38,12 +39,28 @@ class NotificationTransportSerializer(ModelSerializer):
] ]
class NotificationTransportTestSerializer(Serializer):
"""Notification test serializer"""
messages = ListField(child=CharField())
def create(self, request: Request) -> Response:
raise NotImplementedError
def update(self, request: Request) -> Response:
raise NotImplementedError
class NotificationTransportViewSet(ModelViewSet): class NotificationTransportViewSet(ModelViewSet):
"""NotificationTransport Viewset""" """NotificationTransport Viewset"""
queryset = NotificationTransport.objects.all() queryset = NotificationTransport.objects.all()
serializer_class = NotificationTransportSerializer serializer_class = NotificationTransportSerializer
@swagger_auto_schema(
responses={200: NotificationTransportTestSerializer(many=False)},
request_body=no_body,
)
@action(detail=True, methods=["post"]) @action(detail=True, methods=["post"])
# pylint: disable=invalid-name # pylint: disable=invalid-name
def test(self, request: Request, pk=None) -> Response: def test(self, request: Request, pk=None) -> Response:
@ -61,6 +78,10 @@ class NotificationTransportViewSet(ModelViewSet):
user=request.user, user=request.user,
) )
try: try:
return Response(transport.send(notification)) response = NotificationTransportTestSerializer(
data={"messages": transport.send(notification)}
)
response.is_valid()
return Response(response.data)
except NotificationTransportError as exc: except NotificationTransportError as exc:
return Response(str(exc.__cause__ or None), status=503) return Response(str(exc.__cause__ or None), status=503)

View file

@ -38,7 +38,9 @@ class Challenge(Serializer):
"""Challenge that gets sent to the client based on which stage """Challenge that gets sent to the client based on which stage
is currently active""" is currently active"""
type = ChoiceField(choices=list(ChallengeTypes)) type = ChoiceField(
choices=[(x.name, x.name) for x in ChallengeTypes],
)
component = CharField(required=False) component = CharField(required=False)
title = CharField(required=False) title = CharField(required=False)
@ -90,7 +92,7 @@ class ChallengeResponse(Serializer):
stage: Optional["StageView"] stage: Optional["StageView"]
def __init__(self, instance, data, **kwargs): def __init__(self, instance=None, data=None, **kwargs):
self.stage = kwargs.pop("stage", None) self.stage = kwargs.pop("stage", None)
super().__init__(instance=instance, data=data, **kwargs) super().__init__(instance=instance, data=data, **kwargs)

View file

@ -4,6 +4,7 @@ from django.http import HttpRequest
from django.http.request import QueryDict from django.http.request import QueryDict
from django.http.response import HttpResponse from django.http.response import HttpResponse
from django.views.generic.base import View from django.views.generic.base import View
from rest_framework.request import Request
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.core.models import DEFAULT_AVATAR, User from authentik.core.models import DEFAULT_AVATAR, User
@ -67,9 +68,9 @@ class ChallengeStageView(StageView):
return HttpChallengeResponse(challenge) return HttpChallengeResponse(challenge)
# pylint: disable=unused-argument # pylint: disable=unused-argument
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: def post(self, request: Request, *args, **kwargs) -> HttpResponse:
"""Handle challenge response""" """Handle challenge response"""
challenge: ChallengeResponse = self.get_response_instance(data=request.POST) challenge: ChallengeResponse = self.get_response_instance(data=request.data)
if not challenge.is_valid(): if not challenge.is_valid():
return self.challenge_invalid(challenge) return self.challenge_invalid(challenge)
return self.challenge_valid(challenge) return self.challenge_valid(challenge)

View file

@ -9,11 +9,16 @@ from django.template.response import TemplateResponse
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.views.decorators.clickjacking import xframe_options_sameorigin from django.views.decorators.clickjacking import xframe_options_sameorigin
from django.views.generic import TemplateView, View from django.views.generic import TemplateView, View
from drf_yasg2.utils import no_body, swagger_auto_schema
from rest_framework.permissions import AllowAny
from rest_framework.views import APIView
from structlog.stdlib import BoundLogger, get_logger from structlog.stdlib import BoundLogger, get_logger
from authentik.core.models import USER_ATTRIBUTE_DEBUG from authentik.core.models import USER_ATTRIBUTE_DEBUG
from authentik.events.models import cleanse_dict from authentik.events.models import cleanse_dict
from authentik.flows.challenge import ( from authentik.flows.challenge import (
Challenge,
ChallengeResponse,
ChallengeTypes, ChallengeTypes,
HttpChallengeResponse, HttpChallengeResponse,
RedirectChallenge, RedirectChallenge,
@ -40,9 +45,11 @@ SESSION_KEY_GET = "authentik_flows_get"
@method_decorator(xframe_options_sameorigin, name="dispatch") @method_decorator(xframe_options_sameorigin, name="dispatch")
class FlowExecutorView(View): class FlowExecutorView(APIView):
"""Stage 1 Flow executor, passing requests to Stage Views""" """Stage 1 Flow executor, passing requests to Stage Views"""
permission_classes = [AllowAny]
flow: Flow flow: Flow
plan: Optional[FlowPlan] = None plan: Optional[FlowPlan] = None
@ -113,8 +120,13 @@ class FlowExecutorView(View):
self.current_stage_view.request = request self.current_stage_view.request = request
return super().dispatch(request) return super().dispatch(request)
@swagger_auto_schema(
responses={200: Challenge()},
request_body=no_body,
operation_id="flows_executor_get",
)
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
"""pass get request to current stage""" """Get the next pending challenge from the currently active flow."""
self._logger.debug( self._logger.debug(
"f(exec): Passing GET", "f(exec): Passing GET",
view_class=class_to_path(self.current_stage_view.__class__), view_class=class_to_path(self.current_stage_view.__class__),
@ -127,8 +139,13 @@ class FlowExecutorView(View):
self._logger.exception(exc) self._logger.exception(exc)
return to_stage_response(request, FlowErrorResponse(request, exc)) return to_stage_response(request, FlowErrorResponse(request, exc))
@swagger_auto_schema(
responses={200: Challenge()},
request_body=ChallengeResponse(),
operation_id="flows_executor_solve",
)
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
"""pass post request to current stage""" """Solve the previously retrieved challenge and advanced to the next stage."""
self._logger.debug( self._logger.debug(
"f(exec): Passing POST", "f(exec): Passing POST",
view_class=class_to_path(self.current_stage_view.__class__), view_class=class_to_path(self.current_stage_view.__class__),
@ -175,8 +192,10 @@ class FlowExecutorView(View):
"f(exec): Continuing with next stage", "f(exec): Continuing with next stage",
reamining=len(self.plan.stages), reamining=len(self.plan.stages),
) )
kwargs = self.kwargs
kwargs.update({"flow_slug": self.flow.slug})
return redirect_with_qs( return redirect_with_qs(
"authentik_api:flow-executor", self.request.GET, **self.kwargs "authentik_api:flow-executor", self.request.GET, **kwargs
) )
# User passed all stages # User passed all stages
self._logger.debug( self._logger.debug(

View file

@ -74,7 +74,7 @@ class SAMLFlowFinalView(ChallengeStageView):
return super().get( return super().get(
self.request, self.request,
**{ **{
"type": ChallengeTypes.native, "type": ChallengeTypes.native.value,
"component": "ak-stage-autosubmit", "component": "ak-stage-autosubmit",
"title": "Redirecting to %(app)s..." % {"app": application.name}, "title": "Redirecting to %(app)s..." % {"app": application.name},
"url": provider.acs_url, "url": provider.acs_url,

View file

@ -31,7 +31,7 @@ class AuthenticatorStaticStageView(ChallengeStageView):
tokens: list[StaticToken] = self.request.session[SESSION_STATIC_TOKENS] tokens: list[StaticToken] = self.request.session[SESSION_STATIC_TOKENS]
return AuthenticatorStaticChallenge( return AuthenticatorStaticChallenge(
data={ data={
"type": ChallengeTypes.native, "type": ChallengeTypes.native.value,
"component": "ak-stage-authenticator-static", "component": "ak-stage-authenticator-static",
"codes": [token.token for token in tokens], "codes": [token.token for token in tokens],
} }

View file

@ -51,7 +51,7 @@ class AuthenticatorTOTPStageView(ChallengeStageView):
device: TOTPDevice = self.request.session[SESSION_TOTP_DEVICE] device: TOTPDevice = self.request.session[SESSION_TOTP_DEVICE]
return AuthenticatorTOTPChallenge( return AuthenticatorTOTPChallenge(
data={ data={
"type": ChallengeTypes.native, "type": ChallengeTypes.native.value,
"component": "ak-stage-authenticator-totp", "component": "ak-stage-authenticator-totp",
"config_url": device.config_url, "config_url": device.config_url,
} }

View file

@ -145,7 +145,7 @@ class AuthenticatorValidateStageView(ChallengeStageView):
challenges = self.request.session["device_challenges"] challenges = self.request.session["device_challenges"]
return AuthenticatorChallenge( return AuthenticatorChallenge(
data={ data={
"type": ChallengeTypes.native, "type": ChallengeTypes.native.value,
"component": "ak-stage-authenticator-validate", "component": "ak-stage-authenticator-validate",
"device_challenges": challenges, "device_challenges": challenges,
} }

View file

@ -122,7 +122,7 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView):
return AuthenticatorWebAuthnChallenge( return AuthenticatorWebAuthnChallenge(
data={ data={
"type": ChallengeTypes.native, "type": ChallengeTypes.native.value,
"component": "ak-stage-authenticator-webauthn", "component": "ak-stage-authenticator-webauthn",
"registration": make_credential_options.registration_dict, "registration": make_credential_options.registration_dict,
} }

View file

@ -63,7 +63,7 @@ class CaptchaStageView(ChallengeStageView):
def get_challenge(self, *args, **kwargs) -> Challenge: def get_challenge(self, *args, **kwargs) -> Challenge:
return CaptchaChallenge( return CaptchaChallenge(
data={ data={
"type": ChallengeTypes.native, "type": ChallengeTypes.native.value,
"component": "ak-stage-captcha", "component": "ak-stage-captcha",
"site_key": self.executor.current_stage.public_key, "site_key": self.executor.current_stage.public_key,
} }

View file

@ -38,7 +38,7 @@ class ConsentStageView(ChallengeStageView):
def get_challenge(self) -> Challenge: def get_challenge(self) -> Challenge:
challenge = ConsentChallenge( challenge = ConsentChallenge(
data={ data={
"type": ChallengeTypes.native, "type": ChallengeTypes.native.value,
"component": "ak-stage-consent", "component": "ak-stage-consent",
} }
) )

View file

@ -24,7 +24,7 @@ class DummyStageView(ChallengeStageView):
def get_challenge(self, *args, **kwargs) -> Challenge: def get_challenge(self, *args, **kwargs) -> Challenge:
return DummyChallenge( return DummyChallenge(
data={ data={
"type": ChallengeTypes.native, "type": ChallengeTypes.native.value,
"component": "", "component": "",
"title": self.executor.current_stage.name, "title": self.executor.current_stage.name,
} }

View file

@ -94,7 +94,7 @@ class EmailStageView(ChallengeStageView):
def get_challenge(self) -> Challenge: def get_challenge(self) -> Challenge:
challenge = EmailChallenge( challenge = EmailChallenge(
data={"type": ChallengeTypes.native, "component": "ak-stage-email"} data={"type": ChallengeTypes.native.value, "component": "ak-stage-email"}
) )
return challenge return challenge

View file

@ -78,7 +78,7 @@ class IdentificationStageView(ChallengeStageView):
current_stage: IdentificationStage = self.executor.current_stage current_stage: IdentificationStage = self.executor.current_stage
challenge = IdentificationChallenge( challenge = IdentificationChallenge(
data={ data={
"type": ChallengeTypes.native, "type": ChallengeTypes.native.value,
"component": "ak-stage-identification", "component": "ak-stage-identification",
"primary_action": _("Log in"), "primary_action": _("Log in"),
"input_type": "text", "input_type": "text",

View file

@ -78,7 +78,7 @@ class PasswordStageView(ChallengeStageView):
def get_challenge(self) -> Challenge: def get_challenge(self) -> Challenge:
challenge = PasswordChallenge( challenge = PasswordChallenge(
data={ data={
"type": ChallengeTypes.native, "type": ChallengeTypes.native.value,
"component": "ak-stage-password", "component": "ak-stage-password",
} }
) )

View file

@ -164,7 +164,7 @@ class PromptStageView(ChallengeStageView):
fields = list(self.executor.current_stage.fields.all().order_by("order")) fields = list(self.executor.current_stage.fields.all().order_by("order"))
challenge = PromptChallenge( challenge = PromptChallenge(
data={ data={
"type": ChallengeTypes.native, "type": ChallengeTypes.native.value,
"component": "ak-stage-prompt", "component": "ak-stage-prompt",
"fields": [PromptSerializer(field).data for field in fields], "fields": [PromptSerializer(field).data for field in fields],
}, },

View file

@ -279,6 +279,7 @@ stages:
displayName: Build static files for e2e displayName: Build static files for e2e
inputs: inputs:
script: | script: |
docker run --rm -v $(pwd):/local openapitools/openapi-generator-cli generate -i /local/swagger.yaml -g typescript-fetch -o /local/web/src/api --additional-properties=typescriptThreePlus=true
cd web cd web
npm i npm i
npm run build npm run build

View file

@ -33,7 +33,7 @@ stages:
- task: PublishPipelineArtifact@1 - task: PublishPipelineArtifact@1
inputs: inputs:
targetPath: 'outpost/pkg/' targetPath: 'outpost/pkg/'
artifact: 'swagger_client' artifact: 'go_swagger_client'
publishLocation: 'pipeline' publishLocation: 'pipeline'
- stage: lint - stage: lint
jobs: jobs:
@ -51,7 +51,7 @@ stages:
- task: DownloadPipelineArtifact@2 - task: DownloadPipelineArtifact@2
inputs: inputs:
buildType: 'current' buildType: 'current'
artifactName: 'swagger_client' artifactName: 'go_swagger_client'
path: "outpost/pkg/" path: "outpost/pkg/"
- task: CmdLine@2 - task: CmdLine@2
inputs: inputs:
@ -70,7 +70,7 @@ stages:
- task: DownloadPipelineArtifact@2 - task: DownloadPipelineArtifact@2
inputs: inputs:
buildType: 'current' buildType: 'current'
artifactName: 'swagger_client' artifactName: 'go_swagger_client'
path: "outpost/pkg/" path: "outpost/pkg/"
- task: Go@0 - task: Go@0
inputs: inputs:
@ -89,7 +89,7 @@ stages:
- task: DownloadPipelineArtifact@2 - task: DownloadPipelineArtifact@2
inputs: inputs:
buildType: 'current' buildType: 'current'
artifactName: 'swagger_client' artifactName: 'go_swagger_client'
path: "outpost/pkg/" path: "outpost/pkg/"
- task: Bash@3 - task: Bash@3
inputs: inputs:

View file

@ -29,10 +29,7 @@ paths:
'200': '200':
description: Login Metrics per 1h description: Login Metrics per 1h
schema: schema:
description: '' $ref: '#/definitions/LoginMetrics'
type: array
items:
$ref: '#/definitions/AdministrationMetrics'
tags: tags:
- admin - admin
parameters: [] parameters: []
@ -318,9 +315,12 @@ paths:
parameters: [] parameters: []
responses: responses:
'200': '200':
description: '' description: Coordinates for diagrams
schema: schema:
$ref: '#/definitions/Application' description: ''
type: array
items:
$ref: '#/definitions/Coordinate'
tags: tags:
- core - core
parameters: parameters:
@ -1620,17 +1620,12 @@ paths:
description: |- description: |-
Send example notification using selected transport. Requires Send example notification using selected transport. Requires
Modify permissions. Modify permissions.
parameters: parameters: []
- name: data
in: body
required: true
schema:
$ref: '#/definitions/NotificationTransport'
responses: responses:
'201': '200':
description: '' description: Notification test serializer
schema: schema:
$ref: '#/definitions/NotificationTransport' $ref: '#/definitions/NotificationTransportTest'
tags: tags:
- events - events
parameters: parameters:
@ -1822,6 +1817,42 @@ paths:
required: true required: true
type: string type: string
format: uuid format: uuid
/flows/executor/{flow_slug}/:
get:
operationId: flows_executor_get
description: Get the next pending challenge from the currently active flow.
parameters: []
responses:
'200':
description: Challenge that gets sent to the client based on which stage
is currently active
schema:
$ref: '#/definitions/Challenge'
tags:
- flows
post:
operationId: flows_executor_solve
description: Solve the previously retrieved challenge and advanced to the next
stage.
parameters:
- name: data
in: body
required: true
schema:
$ref: '#/definitions/ChallengeResponse'
responses:
'200':
description: Challenge that gets sent to the client based on which stage
is currently active
schema:
$ref: '#/definitions/Challenge'
tags:
- flows
parameters:
- name: flow_slug
in: path
required: true
type: string
/flows/instances/: /flows/instances/:
get: get:
operationId: flows_instances_list operationId: flows_instances_list
@ -9296,17 +9327,33 @@ paths:
type: string type: string
format: uuid format: uuid
definitions: definitions:
AdministrationMetrics: Coordinate:
description: Coordinates for diagrams
type: object
properties:
x_cord:
title: X cord
type: integer
readOnly: true
y_cord:
title: Y cord
type: integer
readOnly: true
LoginMetrics:
description: Login Metrics per 1h description: Login Metrics per 1h
type: object type: object
properties: properties:
logins_per_1h: logins_per_1h:
title: Logins per 1h description: ''
type: string type: array
items:
$ref: '#/definitions/Coordinate'
readOnly: true readOnly: true
logins_failed_per_1h: logins_failed_per_1h:
title: Logins failed per 1h description: ''
type: string type: array
items:
$ref: '#/definitions/Coordinate'
readOnly: true readOnly: true
Task: Task:
description: Serialize TaskInfo and TaskResult description: Serialize TaskInfo and TaskResult
@ -9332,11 +9379,11 @@ definitions:
format: date-time format: date-time
status: status:
title: Status title: Status
type: integer type: string
enum: enum:
- 1 - SUCCESSFUL
- 2 - WARNING
- 4 - ERROR
messages: messages:
description: '' description: ''
type: array type: array
@ -9728,8 +9775,6 @@ definitions:
type: integer type: integer
Notification: Notification:
description: Notification Serializer description: Notification Serializer
required:
- event
type: object type: object
properties: properties:
pk: pk:
@ -9897,6 +9942,18 @@ definitions:
webhook_url: webhook_url:
title: Webhook url title: Webhook url
type: string type: string
NotificationTransportTest:
description: Notification test serializer
required:
- messages
type: object
properties:
messages:
description: ''
type: array
items:
type: string
minLength: 1
Flow: Flow:
description: Flow Serializer description: Flow Serializer
required: required:
@ -10050,6 +10107,55 @@ definitions:
format: uuid format: uuid
readOnly: true readOnly: true
uniqueItems: true uniqueItems: true
ErrorDetail:
description: Serializer for rest_framework's error messages
required:
- string
- code
type: object
properties:
string:
title: String
type: string
minLength: 1
code:
title: Code
type: string
minLength: 1
Challenge:
description: Challenge that gets sent to the client based on which stage is currently
active
required:
- type
type: object
properties:
type:
title: Type
type: string
enum:
- native
- shell
- redirect
component:
title: Component
type: string
minLength: 1
title:
title: Title
type: string
minLength: 1
response_errors:
title: Response errors
type: object
additionalProperties:
description: ''
type: array
items:
$ref: '#/definitions/ErrorDetail'
ChallengeResponse:
description: Base class for all challenge responses
type: object
properties: {}
Cache: Cache:
description: Generic cache stats for an object description: Generic cache stats for an object
type: object type: object
@ -10303,22 +10409,23 @@ definitions:
readOnly: true readOnly: true
TypeCreate: TypeCreate:
description: Types of an object that can be created description: Types of an object that can be created
required:
- name
- description
- link
type: object type: object
properties: properties:
name: name:
title: Name title: Name
type: string type: string
readOnly: true
minLength: 1 minLength: 1
description: description:
title: Description title: Description
type: string type: string
readOnly: true
minLength: 1 minLength: 1
link: link:
title: Link title: Link
type: string type: string
readOnly: true
minLength: 1 minLength: 1
ServiceConnectionState: ServiceConnectionState:
description: Serializer for Service connection state description: Serializer for Service connection state

View file

@ -4,3 +4,8 @@ node_modules
dist dist
# don't lint nyc coverage output # don't lint nyc coverage output
coverage coverage
# don't lint generated code
src/api/apis
src/api/models
src/api/index.ts
src/api/runtime.ts

View file

@ -10,6 +10,25 @@ variables:
branchName: ${{ replace(variables['Build.SourceBranchName'], 'refs/heads/', '') }} branchName: ${{ replace(variables['Build.SourceBranchName'], 'refs/heads/', '') }}
stages: stages:
- stage: generate
jobs:
- job: swagger_generate
pool:
vmImage: 'ubuntu-latest'
steps:
- task: NodeTool@0
inputs:
versionSpec: '12.x'
displayName: 'Install Node.js'
- task: CmdLine@2
inputs:
script: |
docker run --rm -v $(pwd):/local openapitools/openapi-generator-cli generate -i /local/swagger.yaml -g typescript-fetch -o /local/web/src/api --additional-properties=typescriptThreePlus=true
- task: PublishPipelineArtifact@1
inputs:
targetPath: 'web/src/api/'
artifact: 'ts_swagger_client'
publishLocation: 'pipeline'
- stage: lint - stage: lint
jobs: jobs:
- job: eslint - job: eslint
@ -20,6 +39,11 @@ stages:
inputs: inputs:
versionSpec: '12.x' versionSpec: '12.x'
displayName: 'Install Node.js' displayName: 'Install Node.js'
- task: DownloadPipelineArtifact@2
inputs:
buildType: 'current'
artifactName: 'ts_swagger_client'
path: "web/src/api/"
- task: Npm@1 - task: Npm@1
inputs: inputs:
command: 'install' command: 'install'
@ -37,6 +61,11 @@ stages:
inputs: inputs:
versionSpec: '12.x' versionSpec: '12.x'
displayName: 'Install Node.js' displayName: 'Install Node.js'
- task: DownloadPipelineArtifact@2
inputs:
buildType: 'current'
artifactName: 'ts_swagger_client'
path: "web/src/api/"
- task: Npm@1 - task: Npm@1
inputs: inputs:
command: 'install' command: 'install'
@ -56,6 +85,11 @@ stages:
inputs: inputs:
versionSpec: '12.x' versionSpec: '12.x'
displayName: 'Install Node.js' displayName: 'Install Node.js'
- task: DownloadPipelineArtifact@2
inputs:
buildType: 'current'
artifactName: 'ts_swagger_client'
path: "web/src/api/"
- task: Npm@1 - task: Npm@1
inputs: inputs:
command: 'install' command: 'install'
@ -71,16 +105,21 @@ stages:
pool: pool:
vmImage: 'ubuntu-latest' vmImage: 'ubuntu-latest'
steps: steps:
- task: Bash@3 - task: DownloadPipelineArtifact@2
inputs: inputs:
targetType: 'inline' buildType: 'current'
script: | artifactName: 'ts_swagger_client'
python ./scripts/az_do_set_branch.py path: "web/src/api/"
- task: Docker@2 - task: Bash@3
inputs: inputs:
containerRegistry: 'beryjuorg-harbor' targetType: 'inline'
repository: 'authentik/static' script: |
command: 'buildAndPush' python ./scripts/az_do_set_branch.py
Dockerfile: 'web/Dockerfile' - task: Docker@2
tags: "gh-$(branchName)" inputs:
buildContext: 'web/' containerRegistry: 'beryjuorg-harbor'
repository: 'authentik/static'
command: 'buildAndPush'
Dockerfile: 'web/Dockerfile'
tags: "gh-$(branchName)"
buildContext: 'web/'

4
web/src/api/.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
apis/**
models/**
index.ts
runtime.ts

View file

@ -0,0 +1,23 @@
# OpenAPI Generator Ignore
# Generated by openapi-generator https://github.com/openapitools/openapi-generator
# Use this file to prevent files from being overwritten by the generator.
# The patterns follow closely to .gitignore or .dockerignore.
# As an example, the C# client generator defines ApiClient.cs.
# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:
#ApiClient.cs
# You can match any string of characters against a directory, file or extension with a single asterisk (*):
#foo/*/qux
# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux
# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
#foo/**/qux
# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux
# You can also negate patterns with an exclamation (!).
# For example, you can ignore all files in a docs folder with the file extension .md:
#docs/*.md
# Then explicitly reverse the ignore rule for a single file:
#!docs/README.md

View file

@ -0,0 +1,167 @@
apis/AdminApi.ts
apis/CoreApi.ts
apis/CryptoApi.ts
apis/EventsApi.ts
apis/FlowsApi.ts
apis/OutpostsApi.ts
apis/PoliciesApi.ts
apis/PropertymappingsApi.ts
apis/ProvidersApi.ts
apis/RootApi.ts
apis/SourcesApi.ts
apis/StagesApi.ts
apis/index.ts
index.ts
models/Application.ts
models/AuthenticateWebAuthnStage.ts
models/AuthenticatorStaticStage.ts
models/AuthenticatorTOTPStage.ts
models/AuthenticatorValidateStage.ts
models/Cache.ts
models/CaptchaStage.ts
models/CertificateData.ts
models/CertificateKeyPair.ts
models/Challenge.ts
models/Config.ts
models/ConsentStage.ts
models/Coordinate.ts
models/DenyStage.ts
models/DockerServiceConnection.ts
models/DummyPolicy.ts
models/DummyStage.ts
models/EmailStage.ts
models/ErrorDetail.ts
models/Event.ts
models/EventMatcherPolicy.ts
models/EventTopPerUser.ts
models/ExpressionPolicy.ts
models/Flow.ts
models/FlowDiagram.ts
models/FlowStageBinding.ts
models/Group.ts
models/GroupMembershipPolicy.ts
models/HaveIBeenPwendPolicy.ts
models/IPReputation.ts
models/IdentificationStage.ts
models/InlineResponse200.ts
models/InlineResponse2001.ts
models/InlineResponse20010.ts
models/InlineResponse20011.ts
models/InlineResponse20012.ts
models/InlineResponse20013.ts
models/InlineResponse20014.ts
models/InlineResponse20015.ts
models/InlineResponse20016.ts
models/InlineResponse20017.ts
models/InlineResponse20018.ts
models/InlineResponse20019.ts
models/InlineResponse2002.ts
models/InlineResponse20020.ts
models/InlineResponse20021.ts
models/InlineResponse20022.ts
models/InlineResponse20023.ts
models/InlineResponse20024.ts
models/InlineResponse20025.ts
models/InlineResponse20026.ts
models/InlineResponse20027.ts
models/InlineResponse20028.ts
models/InlineResponse20029.ts
models/InlineResponse2003.ts
models/InlineResponse20030.ts
models/InlineResponse20031.ts
models/InlineResponse20032.ts
models/InlineResponse20033.ts
models/InlineResponse20034.ts
models/InlineResponse20035.ts
models/InlineResponse20036.ts
models/InlineResponse20037.ts
models/InlineResponse20038.ts
models/InlineResponse20039.ts
models/InlineResponse2004.ts
models/InlineResponse20040.ts
models/InlineResponse20041.ts
models/InlineResponse20042.ts
models/InlineResponse20043.ts
models/InlineResponse20044.ts
models/InlineResponse20045.ts
models/InlineResponse20046.ts
models/InlineResponse20047.ts
models/InlineResponse20048.ts
models/InlineResponse20049.ts
models/InlineResponse2005.ts
models/InlineResponse20050.ts
models/InlineResponse20051.ts
models/InlineResponse20052.ts
models/InlineResponse20053.ts
models/InlineResponse20054.ts
models/InlineResponse20055.ts
models/InlineResponse20056.ts
models/InlineResponse20057.ts
models/InlineResponse20058.ts
models/InlineResponse20059.ts
models/InlineResponse2006.ts
models/InlineResponse20060.ts
models/InlineResponse2007.ts
models/InlineResponse2008.ts
models/InlineResponse2009.ts
models/InlineResponse200Pagination.ts
models/Invitation.ts
models/InvitationStage.ts
models/KubernetesServiceConnection.ts
models/LDAPPropertyMapping.ts
models/LDAPSource.ts
models/LDAPSourceSyncStatus.ts
models/LoginMetrics.ts
models/Notification.ts
models/NotificationRule.ts
models/NotificationRuleGroup.ts
models/NotificationRuleGroupParent.ts
models/NotificationRuleTransports.ts
models/NotificationTransport.ts
models/NotificationTransportTest.ts
models/OAuth2Provider.ts
models/OAuth2ProviderSetupURLs.ts
models/OAuthSource.ts
models/OpenIDConnectConfiguration.ts
models/Outpost.ts
models/OutpostHealth.ts
models/PasswordExpiryPolicy.ts
models/PasswordPolicy.ts
models/PasswordStage.ts
models/Policy.ts
models/PolicyBinding.ts
models/PolicyBindingPolicy.ts
models/PolicyBindingUser.ts
models/PolicyBindingUserAkGroups.ts
models/PolicyBindingUserGroups.ts
models/PolicyBindingUserSources.ts
models/PolicyBindingUserUserPermissions.ts
models/Prompt.ts
models/PromptStage.ts
models/PropertyMapping.ts
models/Provider.ts
models/ProxyOutpostConfig.ts
models/ProxyProvider.ts
models/ReputationPolicy.ts
models/SAMLMetadata.ts
models/SAMLPropertyMapping.ts
models/SAMLProvider.ts
models/SAMLSource.ts
models/ScopeMapping.ts
models/ServiceConnection.ts
models/ServiceConnectionState.ts
models/Source.ts
models/Stage.ts
models/Task.ts
models/Token.ts
models/TokenView.ts
models/TypeCreate.ts
models/User.ts
models/UserDeleteStage.ts
models/UserLoginStage.ts
models/UserLogoutStage.ts
models/UserReputation.ts
models/UserWriteStage.ts
models/Version.ts
models/index.ts
runtime.ts

View file

@ -0,0 +1 @@
5.1.0-SNAPSHOT

View file

@ -1,32 +0,0 @@
import { DefaultClient, AKResponse, QueryArguments } from "./Client";
import { Provider } from "./Providers";
export class Application {
pk: string;
name: string;
slug: string;
provider?: Provider;
launch_url: string;
meta_launch_url: string;
meta_icon: string;
meta_description: string;
meta_publisher: string;
policies: string[];
constructor() {
throw Error();
}
static get(slug: string): Promise<Application> {
return DefaultClient.fetch<Application>(["core", "applications", slug]);
}
static list(filter?: QueryArguments): Promise<AKResponse<Application>> {
return DefaultClient.fetch<AKResponse<Application>>(["core", "applications"], filter);
}
static adminUrl(rest: string): string {
return `/administration/applications/${rest}`;
}
}

View file

@ -1,26 +0,0 @@
import { DefaultClient, AKResponse, QueryArguments } from "./Client";
export class CertificateKeyPair {
pk: string;
name: string;
fingerprint: string;
cert_expiry: number;
cert_subject: string;
private_key_available: boolean;
constructor() {
throw Error();
}
static get(slug: string): Promise<CertificateKeyPair> {
return DefaultClient.fetch<CertificateKeyPair>(["crypto", "certificatekeypairs", slug]);
}
static list(filter?: QueryArguments): Promise<AKResponse<CertificateKeyPair>> {
return DefaultClient.fetch<AKResponse<CertificateKeyPair>>(["crypto", "certificatekeypairs"], filter);
}
static adminUrl(rest: string): string {
return `/administration/crypto/certificates/${rest}`;
}
}

View file

@ -1,10 +1,3 @@
import { gettext } from "django";
import { showMessage } from "../elements/messages/MessageContainer";
import { getCookie } from "../utils";
import { NotFoundError, RequestError } from "./Error";
export const VERSION = "v2beta";
export interface QueryArguments { export interface QueryArguments {
page?: number; page?: number;
page_size?: number; page_size?: number;
@ -20,97 +13,20 @@ export interface BaseInheritanceModel {
} }
export class Client { export interface AKPagination {
makeUrl(url: string[], query?: QueryArguments): string {
let builtUrl = `/api/${VERSION}/${url.join("/")}/`;
if (query) {
const queryString = Object.keys(query)
.filter((k) => query[k] !== null)
// we default to a string in query[k] as we've filtered out the null above
// this is just for type-hinting
.map((k) => encodeURIComponent(k) + "=" + encodeURIComponent(query[k] || ""))
.join("&");
builtUrl += `?${queryString}`;
}
return builtUrl;
}
fetch<T>(url: string[], query?: QueryArguments): Promise<T> {
const finalUrl = this.makeUrl(url, query);
return fetch(finalUrl)
.then((r) => {
if (r.status > 300) {
switch (r.status) {
case 404:
throw new NotFoundError(`URL ${finalUrl} not found`);
default:
throw new RequestError(r.statusText);
}
}
return r;
})
.catch((e) => {
showMessage({
level_tag: "error",
message: gettext(`Unexpected error while fetching: ${e.toString()}`),
});
return e;
})
.then((r) => r.json())
.then((r) => <T>r);
}
private writeRequest<T>(url: string[], body: T, method: string, query?: QueryArguments): Promise<T> {
const finalUrl = this.makeUrl(url, query);
const csrftoken = getCookie("authentik_csrf");
const request = new Request(finalUrl, {
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
"X-CSRFToken": csrftoken,
},
});
return fetch(request, {
method: method,
mode: "same-origin",
body: JSON.stringify(body),
})
.then((r) => {
if (r.status > 300) {
switch (r.status) {
case 404:
throw new NotFoundError(`URL ${finalUrl} not found`);
default:
throw new RequestError(r.statusText);
}
}
return r;
})
.then((r) => r.json())
.then((r) => <T>r);
}
update<T>(url: string[], body: T, query?: QueryArguments): Promise<T> {
return this.writeRequest(url, body, "PATCH", query);
}
}
export const DefaultClient = new Client();
export interface PBPagination {
next?: number; next?: number;
previous?: number; previous?: number;
count: number; count: number;
current: number; current: number;
total_pages: number; totalPages: number;
start_index: number; startIndex: number;
end_index: number; endIndex: number;
} }
export interface AKResponse<T> { export interface AKResponse<T> {
pagination: PBPagination; pagination: AKPagination;
results: Array<T>; results: Array<T>;
} }

View file

@ -1,42 +1,39 @@
import { DefaultClient } from "./Client";
import * as Sentry from "@sentry/browser"; import * as Sentry from "@sentry/browser";
import { Integrations } from "@sentry/tracing"; import { Integrations } from "@sentry/tracing";
import { VERSION } from "../constants"; import { VERSION } from "../constants";
import { SentryIgnoredError } from "../common/errors"; import { SentryIgnoredError } from "../common/errors";
import { Configuration } from "./runtime";
import { RootApi } from "./apis";
import { Config } from ".";
import { getCookie } from "../utils";
export class Config { export const DEFAULT_CONFIG = new Configuration({
branding_logo: string; basePath: "/api/v2beta",
branding_title: string; headers: {
"X-CSRFToken": getCookie("authentik_csrf"),
error_reporting_enabled: boolean;
error_reporting_environment: string;
error_reporting_send_pii: boolean;
constructor() {
throw Error();
} }
});
static get(): Promise<Config> { export function configureSentry(): Promise<Config> {
return DefaultClient.fetch<Config>(["root", "config"]).then((config) => { return new RootApi(DEFAULT_CONFIG).rootConfigList().then((config) => {
if (config.error_reporting_enabled) { if (config.errorReportingEnabled) {
Sentry.init({ Sentry.init({
dsn: "https://a579bb09306d4f8b8d8847c052d3a1d3@sentry.beryju.org/8", dsn: "https://a579bb09306d4f8b8d8847c052d3a1d3@sentry.beryju.org/8",
release: `authentik@${VERSION}`, release: `authentik@${VERSION}`,
integrations: [ integrations: [
new Integrations.BrowserTracing(), new Integrations.BrowserTracing(),
], ],
tracesSampleRate: 0.6, tracesSampleRate: 0.6,
environment: config.error_reporting_environment, environment: config.errorReportingEnvironment,
beforeSend(event: Sentry.Event, hint: Sentry.EventHint) { beforeSend(event: Sentry.Event, hint: Sentry.EventHint) {
if (hint.originalException instanceof SentryIgnoredError) { if (hint.originalException instanceof SentryIgnoredError) {
return null; return null;
} }
return event; return event;
}, },
}); });
console.debug("authentik/config: Sentry enabled."); console.debug("authentik/config: Sentry enabled.");
} }
return config; return config;
}); });
}
} }

View file

@ -1,30 +0,0 @@
import { DefaultClient, QueryArguments, AKResponse } from "./Client";
import { Event } from "./Events";
export class Notification {
pk: string;
severity: string;
body: string;
created: string;
event?: Event;
seen: boolean;
constructor() {
throw Error();
}
static get(pk: string): Promise<Notification> {
return DefaultClient.fetch<Notification>(["events", "notifications", pk]);
}
static list(filter?: QueryArguments): Promise<AKResponse<Notification>> {
return DefaultClient.fetch<AKResponse<Notification>>(["events", "notifications"], filter);
}
static markSeen(pk: string): Promise<{seen: boolean}> {
return DefaultClient.update(["events", "notifications", pk], {
"seen": true
});
}
}

View file

@ -1,26 +0,0 @@
import { DefaultClient, QueryArguments, AKResponse } from "./Client";
import { Group } from "./Groups";
export class Rule {
pk: string;
name: string;
transports: string[];
severity: string;
group?: Group;
constructor() {
throw Error();
}
static get(pk: string): Promise<Rule> {
return DefaultClient.fetch<Rule>(["events", "rules", pk]);
}
static list(filter?: QueryArguments): Promise<AKResponse<Rule>> {
return DefaultClient.fetch<AKResponse<Rule>>(["events", "rules"], filter);
}
static adminUrl(rest: string): string {
return `/administration/events/rules/${rest}`;
}
}

View file

@ -1,25 +0,0 @@
import { DefaultClient, QueryArguments, AKResponse } from "./Client";
export class Transport {
pk: string;
name: string;
mode: string;
mode_verbose: string;
webhook_url: string;
constructor() {
throw Error();
}
static get(pk: string): Promise<Transport> {
return DefaultClient.fetch<Transport>(["events", "transports", pk]);
}
static list(filter?: QueryArguments): Promise<AKResponse<Transport>> {
return DefaultClient.fetch<AKResponse<Transport>>(["events", "transports"], filter);
}
static adminUrl(rest: string): string {
return `/administration/events/transports/${rest}`;
}
}

View file

@ -1,4 +1,4 @@
import { DefaultClient, AKResponse, QueryArguments } from "./Client"; import { Event } from "./models";
export interface EventUser { export interface EventUser {
pk: number; pk: number;
@ -11,37 +11,7 @@ export interface EventContext {
[key: string]: EventContext | string | number | string[]; [key: string]: EventContext | string | number | string[];
} }
export class Event { export interface EventWithContext extends Event {
pk: string;
user: EventUser; user: EventUser;
action: string;
app: string;
context: EventContext; context: EventContext;
client_ip: string;
created: string;
constructor() {
throw Error();
}
static get(pk: string): Promise<Event> {
return DefaultClient.fetch<Event>(["events", "events", pk]);
}
static list(filter?: QueryArguments): Promise<AKResponse<Event>> {
return DefaultClient.fetch<AKResponse<Event>>(["events", "events"], filter);
}
// events/events/top_per_user/?filter_action=authorize_application
static topForUser(action: string): Promise<TopNEvent[]> {
return DefaultClient.fetch<TopNEvent[]>(["events", "events", "top_per_user"], {
"filter_action": action,
});
}
}
export interface TopNEvent {
application: { [key: string]: string};
counted_events: number;
unique_users: number;
} }

View file

@ -1,12 +1,4 @@
import { DefaultClient, AKResponse, QueryArguments, BaseInheritanceModel } from "./Client"; import { ChallengeTypeEnum } from "./models";
import { TypeCreate } from "./Providers";
export enum ChallengeTypes {
native = "native",
response = "response",
shell = "shell",
redirect = "redirect",
}
export interface Error { export interface Error {
code: string; code: string;
@ -18,11 +10,12 @@ export interface ErrorDict {
} }
export interface Challenge { export interface Challenge {
type: ChallengeTypes; type: ChallengeTypeEnum;
component?: string; component?: string;
title?: string; title?: string;
response_errors?: ErrorDict; response_errors?: ErrorDict;
} }
export interface WithUserInfoChallenge extends Challenge { export interface WithUserInfoChallenge extends Challenge {
pending_user: string; pending_user: string;
pending_user_avatar: string; pending_user_avatar: string;
@ -31,6 +24,7 @@ export interface WithUserInfoChallenge extends Challenge {
export interface ShellChallenge extends Challenge { export interface ShellChallenge extends Challenge {
body: string; body: string;
} }
export interface RedirectChallenge extends Challenge { export interface RedirectChallenge extends Challenge {
to: string; to: string;
} }
@ -44,104 +38,3 @@ export enum FlowDesignation {
Recovery = "recovery", Recovery = "recovery",
StageConfiguration = "stage_configuration", StageConfiguration = "stage_configuration",
} }
export class Flow {
pk: string;
policybindingmodel_ptr_id: string;
name: string;
slug: string;
title: string;
designation: FlowDesignation;
background: string;
stages: string[];
policies: string[];
cache_count: number;
constructor() {
throw Error();
}
static get(slug: string): Promise<Flow> {
return DefaultClient.fetch<Flow>(["flows", "instances", slug]);
}
static diagram(slug: string): Promise<{ diagram: string }> {
return DefaultClient.fetch<{ diagram: string }>(["flows", "instances", slug, "diagram"]);
}
static list(filter?: QueryArguments): Promise<AKResponse<Flow>> {
return DefaultClient.fetch<AKResponse<Flow>>(["flows", "instances"], filter);
}
static cached(): Promise<number> {
return DefaultClient.fetch<{ count: number }>(["flows", "instances", "cached"]).then(r => {
return r.count;
});
}
static executor(slug: string): Promise<Challenge> {
return DefaultClient.fetch(["flows", "executor", slug]);
}
static adminUrl(rest: string): string {
return `/administration/flows/${rest}`;
}
}
export class Stage implements BaseInheritanceModel {
pk: string;
name: string;
object_type: string;
verbose_name: string;
verbose_name_plural: string;
flow_set: Flow[];
constructor() {
throw Error();
}
static get(slug: string): Promise<Stage> {
return DefaultClient.fetch<Stage>(["stages", "all", slug]);
}
static list(filter?: QueryArguments): Promise<AKResponse<Stage>> {
return DefaultClient.fetch<AKResponse<Stage>>(["stages", "all"], filter);
}
static getTypes(): Promise<TypeCreate[]> {
return DefaultClient.fetch<TypeCreate[]>(["stages", "all", "types"]);
}
static adminUrl(rest: string): string {
return `/administration/stages/${rest}`;
}
}
export class FlowStageBinding {
pk: string;
policybindingmodel_ptr_id: string;
target: string;
stage: string;
stage_obj: Stage;
evaluate_on_plan: boolean;
re_evaluate_policies: boolean;
order: number;
policies: string[];
constructor() {
throw Error();
}
static get(slug: string): Promise<FlowStageBinding> {
return DefaultClient.fetch<FlowStageBinding>(["flows", "bindings", slug]);
}
static list(filter?: QueryArguments): Promise<AKResponse<FlowStageBinding>> {
return DefaultClient.fetch<AKResponse<FlowStageBinding>>(["flows", "bindings"], filter);
}
static adminUrl(rest: string): string {
return `/administration/stages/bindings/${rest}`;
}
}

View file

@ -1,28 +0,0 @@
import { DefaultClient, QueryArguments, AKResponse } from "./Client";
import { EventContext } from "./Events";
export class Group {
pk: string;
name: string;
is_superuser: boolean;
attributes: EventContext;
parent?: Group;
users: number[];
constructor() {
throw Error();
}
static get(pk: string): Promise<Group> {
return DefaultClient.fetch<Group>(["core", "groups", pk]);
}
static list(filter?: QueryArguments): Promise<AKResponse<Group>> {
return DefaultClient.fetch<AKResponse<Group>>(["core", "groups"], filter);
}
static adminUrl(rest: string): string {
return `/administration/groups/${rest}`;
}
}

View file

@ -1,27 +0,0 @@
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}`;
}
}

View file

@ -1,79 +0,0 @@
import { DefaultClient, AKResponse, QueryArguments } from "./Client";
import { Provider, TypeCreate } from "./Providers";
export interface OutpostHealth {
last_seen: number;
version: string;
version_should: string;
version_outdated: boolean;
}
export class Outpost {
pk: string;
name: string;
providers: number[];
providers_obj: Provider[];
service_connection?: string;
_config: QueryArguments;
token_identifier: string;
constructor() {
throw Error();
}
static get(pk: string): Promise<Outpost> {
return DefaultClient.fetch<Outpost>(["outposts", "outposts", pk]);
}
static list(filter?: QueryArguments): Promise<AKResponse<Outpost>> {
return DefaultClient.fetch<AKResponse<Outpost>>(["outposts", "outposts"], filter);
}
static health(pk: string): Promise<OutpostHealth[]> {
return DefaultClient.fetch<OutpostHealth[]>(["outposts", "outposts", pk, "health"]);
}
static adminUrl(rest: string): string {
return `/administration/outposts/${rest}`;
}
}
export interface OutpostServiceConnectionState {
version: string;
healthy: boolean;
}
export class OutpostServiceConnection {
pk: string;
name: string;
local: boolean;
object_type: string;
verbose_name: string;
verbose_name_plural: string;
constructor() {
throw Error();
}
static get(pk: string): Promise<OutpostServiceConnection> {
return DefaultClient.fetch<OutpostServiceConnection>(["outposts", "service_connections", "all", pk]);
}
static list(filter?: QueryArguments): Promise<AKResponse<OutpostServiceConnection>> {
return DefaultClient.fetch<AKResponse<OutpostServiceConnection>>(["outposts", "service_connections", "all"], filter);
}
static state(pk: string): Promise<OutpostServiceConnectionState> {
return DefaultClient.fetch<OutpostServiceConnectionState>(["outposts", "service_connections", "all", pk, "state"]);
}
static getTypes(): Promise<TypeCreate[]> {
return DefaultClient.fetch<TypeCreate[]>(["outposts", "service_connections", "all", "types"]);
}
static adminUrl(rest: string): string {
return `/administration/outpost_service_connections/${rest}`;
}
}

View file

@ -1,38 +0,0 @@
import { DefaultClient, BaseInheritanceModel, AKResponse, QueryArguments } from "./Client";
import { TypeCreate } from "./Providers";
export class Policy implements BaseInheritanceModel {
pk: string;
name: string;
execution_logging: boolean;
object_type: string;
verbose_name: string;
verbose_name_plural: string;
bound_to: number;
constructor() {
throw Error();
}
static get(pk: string): Promise<Policy> {
return DefaultClient.fetch<Policy>(["policies", "all", pk]);
}
static list(filter?: QueryArguments): Promise<AKResponse<Policy>> {
return DefaultClient.fetch<AKResponse<Policy>>(["policies", "all"], filter);
}
static cached(): Promise<number> {
return DefaultClient.fetch<{ count: number }>(["policies", "all", "cached"]).then(r => {
return r.count;
});
}
static getTypes(): Promise<TypeCreate[]> {
return DefaultClient.fetch<TypeCreate[]>(["policies", "all", "types"]);
}
static adminUrl(rest: string): string {
return `/administration/policies/${rest}`;
}
}

View file

@ -1,31 +0,0 @@
import { DefaultClient, AKResponse, QueryArguments } from "./Client";
import { Group } from "./Groups";
import { Policy } from "./Policies";
import { User } from "./Users";
export class PolicyBinding {
pk: string;
policy?: Policy;
group?: Group;
user?: User;
target: string;
enabled: boolean;
order: number;
timeout: number;
constructor() {
throw Error();
}
static get(pk: string): Promise<PolicyBinding> {
return DefaultClient.fetch<PolicyBinding>(["policies", "bindings", pk]);
}
static list(filter?: QueryArguments): Promise<AKResponse<PolicyBinding>> {
return DefaultClient.fetch<AKResponse<PolicyBinding>>(["policies", "bindings"], filter);
}
static adminUrl(rest: string): string {
return `/administration/policies/bindings/${rest}`;
}
}

View file

@ -1,30 +0,0 @@
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

@ -1,31 +0,0 @@
import { DefaultClient, AKResponse, QueryArguments } from "./Client";
import { TypeCreate } from "./Providers";
export class PropertyMapping {
pk: string;
name: string;
expression: string;
verbose_name: string;
verbose_name_plural: string;
constructor() {
throw Error();
}
static get(pk: string): Promise<PropertyMapping> {
return DefaultClient.fetch<PropertyMapping>(["propertymappings", "all", pk]);
}
static list(filter?: QueryArguments): Promise<AKResponse<PropertyMapping>> {
return DefaultClient.fetch<AKResponse<PropertyMapping>>(["propertymappings", "all"], filter);
}
static getTypes(): Promise<TypeCreate[]> {
return DefaultClient.fetch<TypeCreate[]>(["propertymappings", "all", "types"]);
}
static adminUrl(rest: string): string {
return `/administration/property-mappings/${rest}`;
}
}

View file

@ -1,40 +0,0 @@
import { BaseInheritanceModel, DefaultClient, AKResponse, QueryArguments } from "./Client";
export interface TypeCreate {
name: string;
description: string;
link: string;
}
export class Provider implements BaseInheritanceModel {
pk: number;
name: string;
authorization_flow: string;
object_type: string;
assigned_application_slug?: string;
assigned_application_name?: string;
verbose_name: string;
verbose_name_plural: string;
constructor() {
throw Error();
}
static get(id: number): Promise<Provider> {
return DefaultClient.fetch<Provider>(["providers", "all", id.toString()]);
}
static list(filter?: QueryArguments): Promise<AKResponse<Provider>> {
return DefaultClient.fetch<AKResponse<Provider>>(["providers", "all"], filter);
}
static getTypes(): Promise<TypeCreate[]> {
return DefaultClient.fetch<TypeCreate[]>(["providers", "all", "types"]);
}
static adminUrl(rest: string): string {
return `/administration/providers/${rest}`;
}
}

View file

@ -1,34 +0,0 @@
import { BaseInheritanceModel, DefaultClient, AKResponse, QueryArguments } from "./Client";
import { TypeCreate } from "./Providers";
export class Source implements BaseInheritanceModel {
pk: string;
name: string;
slug: string;
enabled: boolean;
authentication_flow: string;
enrollment_flow: string;
constructor() {
throw Error();
}
object_type: string;
verbose_name: string;
verbose_name_plural: string;
static get(slug: string): Promise<Source> {
return DefaultClient.fetch<Source>(["sources", "all", slug]);
}
static list(filter?: QueryArguments): Promise<AKResponse<Source>> {
return DefaultClient.fetch<AKResponse<Source>>(["sources", "all"], filter);
}
static getTypes(): Promise<TypeCreate[]> {
return DefaultClient.fetch<TypeCreate[]>(["sources", "all", "types"]);
}
static adminUrl(rest: string): string {
return `/administration/sources/${rest}`;
}
}

View file

@ -1,33 +0,0 @@
import { DefaultClient, QueryArguments } from "./Client";
export enum TaskStatus {
SUCCESSFUL = 1,
WARNING = 2,
ERROR = 4,
}
export class SystemTask {
task_name: string;
task_description: string;
task_finish_timestamp: number;
status: TaskStatus;
messages: string[];
constructor() {
throw Error();
}
static get(task_name: string): Promise<SystemTask> {
return DefaultClient.fetch<SystemTask>(["admin", "system_tasks", task_name]);
}
static list(filter?: QueryArguments): Promise<SystemTask[]> {
return DefaultClient.fetch<SystemTask[]>(["admin", "system_tasks"], filter);
}
static retry(task_name: string): string {
return DefaultClient.makeUrl(["admin", "system_tasks", task_name, "retry"]);
}
}

View file

@ -1,47 +0,0 @@
import { AKResponse, DefaultClient, QueryArguments } from "./Client";
import { User } from "./Users";
export enum TokenIntent {
INTENT_VERIFICATION = "verification",
INTENT_API = "api",
INTENT_RECOVERY = "recovery",
}
export class Token {
pk: string;
identifier: string;
intent: TokenIntent;
user: User;
description: string;
expires: number;
expiring: boolean;
constructor() {
throw Error();
}
static get(pk: string): Promise<User> {
return DefaultClient.fetch<User>(["core", "tokens", pk]);
}
static list(filter?: QueryArguments): Promise<AKResponse<Token>> {
return DefaultClient.fetch<AKResponse<Token>>(["core", "tokens"], filter);
}
static adminUrl(rest: string): string {
return `/administration/tokens/${rest}`;
}
static userUrl(rest: string): string {
return `/-/user/tokens/${rest}`;
}
static getKey(identifier: string): Promise<string> {
return DefaultClient.fetch<{ key: string }>(["core", "tokens", identifier, "view_key"]).then(
(r) => r.key
);
}
}

View file

@ -1,45 +1,11 @@
import { DefaultClient, AKResponse, QueryArguments } from "./Client"; import { CoreApi } from "./apis";
import { DEFAULT_CONFIG } from "./Config";
import { User } from "./models";
let _globalMePromise: Promise<User>; let _globalMePromise: Promise<User>;
export function me(): Promise<User> {
export class User { if (!_globalMePromise) {
pk: number; _globalMePromise = new CoreApi(DEFAULT_CONFIG).coreUsersMe({});
username: string;
name: string;
is_superuser: boolean;
email: boolean;
avatar: string;
is_active: boolean;
last_login: number;
constructor() {
throw Error();
}
static get(pk: string): Promise<User> {
return DefaultClient.fetch<User>(["core", "users", pk]);
}
static list(filter?: QueryArguments): Promise<AKResponse<User>> {
return DefaultClient.fetch<AKResponse<User>>(["core", "users"], filter);
}
static adminUrl(rest: string): string {
return `/administration/users/${rest}`;
}
static me(): Promise<User> {
if (!_globalMePromise) {
_globalMePromise = DefaultClient.fetch<User>(["core", "users", "me"]);
}
return _globalMePromise;
}
static count(): Promise<number> {
return DefaultClient.fetch<AKResponse<User>>(["core", "users"], {
"page_size": 1
}).then(r => {
return r.pagination.count;
});
} }
return _globalMePromise;
} }

View file

@ -1,17 +0,0 @@
import { DefaultClient } from "./Client";
export class Version {
version_current: string;
version_latest: string;
outdated: boolean;
constructor() {
throw Error();
}
static get(): Promise<Version> {
return DefaultClient.fetch<Version>(["admin", "version"]);
}
}

97
web/src/api/legacy.ts Normal file
View file

@ -0,0 +1,97 @@
export class AdminURLManager {
static applications(rest: string): string {
return `/administration/applications/${rest}`;
}
static cryptoCertificates(rest: string): string {
return `/administration/crypto/certificates/${rest}`;
}
static policies(rest: string): string {
return `/administration/policies/${rest}`;
}
static policyBindings(rest: string): string {
return `/administration/policies/bindings/${rest}`;
}
static providers(rest: string): string {
return `/administration/providers/${rest}`;
}
static propertyMappings(rest: string): string {
return `/administration/property-mappings/${rest}`;
}
static outposts(rest: string): string {
return `/administration/outposts/${rest}`;
}
static outpostServiceConnections(rest: string): string {
return `/administration/outpost_service_connections/${rest}`;
}
static flows(rest: string): string {
return `/administration/flows/${rest}`;
}
static stages(rest: string): string {
return `/administration/stages/${rest}`;
}
static stagePrompts(rest: string): string {
return `/administration/stages_prompts/${rest}`;
}
static stageInvitations(rest: string): string {
return `/administration/stages/invitations/${rest}`;
}
static stageBindings(rest: string): string {
return `/administration/stages/bindings/${rest}`;
}
static sources(rest: string): string {
return `/administration/sources/${rest}`;
}
static tokens(rest: string): string {
return `/administration/tokens/${rest}`;
}
static eventRules(rest: string): string {
return `/administration/events/rules/${rest}`;
}
static eventTransports(rest: string): string {
return `/administration/events/transports/${rest}`;
}
static users(rest: string): string {
return `/administration/users/${rest}`;
}
static groups(rest: string): string {
return `/administration/groups/${rest}`;
}
}
export class UserURLManager {
static tokens(rest: string): string {
return `/-/user/tokens/${rest}`;
}
}
export class AppURLManager {
static sourceSAML(slug: string, rest: string): string {
return `/source/saml/${slug}/${rest}`;
}
static providerSAML(rest: string): string {
return `/application/saml/${rest}`;
}
}

View file

@ -1,43 +0,0 @@
import { DefaultClient } from "../Client";
import { Provider } from "../Providers";
export interface OAuth2SetupURLs {
issuer?: string;
authorize: string;
token: string;
user_info: string;
provider_info?: string;
logout?: string;
}
export class OAuth2Provider extends Provider {
client_type: string
client_id: string;
client_secret: string;
token_validity: string;
include_claims_in_id_token: boolean;
jwt_alg: string;
rsa_key: string;
redirect_uris: string;
sub_mode: string;
issuer_mode: string;
constructor() {
super();
throw Error();
}
static get(id: number): Promise<OAuth2Provider> {
return DefaultClient.fetch<OAuth2Provider>(["providers", "oauth2", id.toString()]);
}
static getLaunchURls(id: number): Promise<OAuth2SetupURLs> {
return DefaultClient.fetch(["providers", "oauth2", id.toString(), "setup_urls"]);
}
static appUrl(rest: string): string {
return `/application/oauth2/${rest}`;
}
}

View file

@ -1,30 +0,0 @@
import { DefaultClient } from "../Client";
import { Provider } from "../Providers";
export class ProxyProvider extends Provider {
internal_host: string;
external_host: string;
internal_host_ssl_validation: boolean
certificate?: string;
skip_path_regex: string;
basic_auth_enabled: boolean;
basic_auth_password_attribute: string;
basic_auth_user_attribute: string;
constructor() {
super();
throw Error();
}
static get(id: number): Promise<ProxyProvider> {
return DefaultClient.fetch<ProxyProvider>(["providers", "proxy", id.toString()]);
}
static getMetadata(id: number): Promise<{ metadata: string }> {
return DefaultClient.fetch(["providers", "proxy", id.toString(), "metadata"]);
}
static appUrl(rest: string): string {
return `/application/proxy/${rest}`;
}
}

View file

@ -1,33 +0,0 @@
import { DefaultClient } from "../Client";
import { Provider } from "../Providers";
export class SAMLProvider extends Provider {
acs_url: string;
audience: string;
issuer: string;
assertion_valid_not_before: string;
assertion_valid_not_on_or_after: string;
session_valid_not_on_or_after: string;
name_id_mapping?: string;
digest_algorithm: string;
signature_algorithm: string;
signing_kp?: string;
verification_kp?: string;
constructor() {
super();
throw Error();
}
static get(id: number): Promise<SAMLProvider> {
return DefaultClient.fetch<SAMLProvider>(["providers", "saml", id.toString()]);
}
static getMetadata(id: number): Promise<{ metadata: string }> {
return DefaultClient.fetch(["providers", "saml", id.toString(), "metadata"]);
}
static appUrl(rest: string): string {
return `/application/saml/${rest}`;
}
}

View file

@ -1,35 +0,0 @@
import { DefaultClient } from "../Client";
import { Source } from "../Sources";
export class LDAPSource extends Source {
server_uri: string;
bind_cn: string;
start_tls: boolean
base_dn: string;
additional_user_dn: string;
additional_group_dn: string;
user_object_filter: string;
group_object_filter: string;
group_membership_field: string;
object_uniqueness_field: string;
sync_users: boolean;
sync_users_password: boolean;
sync_groups: boolean;
sync_parent_group?: string;
property_mappings: string[];
property_mappings_group: string[];
constructor() {
super();
throw Error();
}
static get(slug: string): Promise<LDAPSource> {
return DefaultClient.fetch<LDAPSource>(["sources", "ldap", slug]);
}
static syncStatus(slug: string): Promise<{ last_sync?: number }> {
return DefaultClient.fetch(["sources", "ldap", slug, "sync_status"]);
}
}

View file

@ -1,22 +0,0 @@
import { DefaultClient } from "../Client";
import { Source } from "../Sources";
export class OAuthSource extends Source {
provider_type: string;
request_token_url: string;
authorization_url: string;
access_token_url: string;
profile_url: string;
consumer_key: string;
callback_url: string;
constructor() {
super();
throw Error();
}
static get(slug: string): Promise<OAuthSource> {
return DefaultClient.fetch<OAuthSource>(["sources", "oauth", slug]);
}
}

View file

@ -1,32 +0,0 @@
import { DefaultClient } from "../Client";
import { Source } from "../Sources";
export class SAMLSource extends Source {
issuer: string;
sso_url: string;
slo_url: string;
allow_idp_initiated: boolean;
name_id_policy: string;
binding_type: string
signing_kp?: string;
digest_algorithm: string;
signature_algorithm: string;
temporary_user_delete_after: string;
constructor() {
super();
throw Error();
}
static get(slug: string): Promise<SAMLSource> {
return DefaultClient.fetch<SAMLSource>(["sources", "saml", slug]);
}
static getMetadata(slug: string): Promise<{ metadata: string }> {
return DefaultClient.fetch(["sources", "saml", slug, "metadata"]);
}
static appUrl(slug: string, rest: string): string {
return `/source/saml/${slug}/${rest}`;
}
}

View file

@ -157,7 +157,7 @@ ak-message {
color: var(--ak-dark-foreground) !important; color: var(--ak-dark-foreground) !important;
} }
/* tabs, vertical */ /* tabs, vertical */
.pf-c-tabs__link { .pf-c-tabs.pf-m-vertical .pf-c-tabs__link {
background-color: var(--ak-dark-background-light); background-color: var(--ak-dark-background-light);
} }
/* table, on mobile */ /* table, on mobile */

View file

@ -1,119 +0,0 @@
import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
import Chart from "chart.js";
import { DefaultClient } from "../api/Client";
interface TickValue {
value: number;
major: boolean;
}
export interface LoginMetrics {
logins_failed_per_1h: { x: number, y: number }[];
logins_per_1h: { x: number, y: number }[];
}
@customElement("ak-admin-logins-chart")
export class AdminLoginsChart extends LitElement {
@property({type: Array})
url: string[] = [];
chart?: Chart;
static get styles(): CSSResult[] {
return [css`
:host {
position: relative;
height: 100%;
width: 100%;
display: block;
min-height: 25rem;
}
canvas {
width: 100px;
height: 100px;
}
`];
}
constructor() {
super();
window.addEventListener("resize", () => {
if (this.chart) {
this.chart.resize();
}
});
}
firstUpdated(): void {
DefaultClient.fetch<LoginMetrics>(this.url)
.then((r) => {
const canvas = <HTMLCanvasElement>this.shadowRoot?.querySelector("canvas");
if (!canvas) {
console.warn("Failed to get canvas element");
return false;
}
const ctx = canvas.getContext("2d");
if (!ctx) {
console.warn("failed to get 2d context");
return false;
}
this.chart = new Chart(ctx, {
type: "bar",
data: {
datasets: [
{
label: "Failed Logins",
backgroundColor: "rgba(201, 25, 11, .5)",
spanGaps: true,
data: r.logins_failed_per_1h,
},
{
label: "Successful Logins",
backgroundColor: "rgba(189, 229, 184, .5)",
spanGaps: true,
data: r.logins_per_1h,
},
],
},
options: {
maintainAspectRatio: false,
spanGaps: true,
scales: {
xAxes: [
{
stacked: true,
gridLines: {
color: "rgba(0, 0, 0, 0)",
},
type: "time",
offset: true,
ticks: {
callback: function (value, index: number, values) {
const valueStamp = <TickValue>(<unknown>values[index]);
const delta = Date.now() - valueStamp.value;
const ago = Math.round(delta / 1000 / 3600);
return `${ago} Hours ago`;
},
autoSkip: true,
maxTicksLimit: 8,
},
},
],
yAxes: [
{
stacked: true,
gridLines: {
color: "rgba(0, 0, 0, 0)",
},
},
],
},
},
});
});
}
render(): TemplateResult {
return html`<canvas></canvas>`;
}
}

View file

@ -1,4 +1,3 @@
import { getCookie } from "../../utils";
import { customElement, property } from "lit-element"; import { customElement, property } from "lit-element";
import { ERROR_CLASS, SUCCESS_CLASS } from "../../constants"; import { ERROR_CLASS, SUCCESS_CLASS } from "../../constants";
import { SpinnerButton } from "./SpinnerButton"; import { SpinnerButton } from "./SpinnerButton";
@ -12,43 +11,33 @@ export class ActionButton extends SpinnerButton {
@property() @property()
method = "POST"; method = "POST";
@property({attribute: false})
// eslint-disable-next-line @typescript-eslint/no-explicit-any
apiRequest: () => Promise<any> = () => { throw new Error(); };
callAction(): void { callAction(): void {
if (this.isRunning === true) { if (this.isRunning === true) {
return; return;
} }
this.setLoading(); this.setLoading();
const csrftoken = getCookie("authentik_csrf"); this.apiRequest().then(() => {
const request = new Request(this.url, { this.setDone(SUCCESS_CLASS);
headers: { "X-CSRFToken": csrftoken },
});
fetch(request, {
method: this.method,
mode: "same-origin",
}) })
.then((r) => { .catch((e: Error | Response) => {
if (!r.ok) { if (e instanceof Error) {
throw r; showMessage({
} level_tag: "error",
return r; message: e.toString()
}) });
.then(() => { } else {
this.setDone(SUCCESS_CLASS); e.text().then(t => {
})
.catch((e: Error | Response) => {
if (e instanceof Error) {
showMessage({ showMessage({
level_tag: "error", level_tag: "error",
message: e.toString() message: t
}); });
} else { });
e.text().then(t => { }
showMessage({ this.setDone(ERROR_CLASS);
level_tag: "error", });
message: t
});
});
}
this.setDone(ERROR_CLASS);
});
} }
} }

View file

@ -3,9 +3,10 @@ import { css, CSSResult, customElement, html, LitElement, property, TemplateResu
import GlobalsStyle from "@patternfly/patternfly/base/patternfly-globals.css"; import GlobalsStyle from "@patternfly/patternfly/base/patternfly-globals.css";
// @ts-ignore // @ts-ignore
import ButtonStyle from "@patternfly/patternfly/components/Button/button.css"; import ButtonStyle from "@patternfly/patternfly/components/Button/button.css";
import { Token } from "../../api/Tokens"; import { CoreApi } from "../../api";
import { ERROR_CLASS, PRIMARY_CLASS, SUCCESS_CLASS } from "../../constants"; import { ERROR_CLASS, PRIMARY_CLASS, SUCCESS_CLASS } from "../../constants";
import { ColorStyles } from "../../common/styles"; import { ColorStyles } from "../../common/styles";
import { DEFAULT_CONFIG } from "../../api/Config";
@customElement("ak-token-copy-button") @customElement("ak-token-copy-button")
export class TokenCopyButton extends LitElement { export class TokenCopyButton extends LitElement {
@ -36,8 +37,14 @@ export class TokenCopyButton extends LitElement {
}, 1500); }, 1500);
return; return;
} }
Token.getKey(this.identifier).then((token) => { new CoreApi(DEFAULT_CONFIG).coreTokensViewKey({
navigator.clipboard.writeText(token).then(() => { identifier: this.identifier
}).then((token) => {
if (!token.key) {
this.buttonClass = ERROR_CLASS;
return;
}
navigator.clipboard.writeText(token.key).then(() => {
this.buttonClass = SUCCESS_CLASS; this.buttonClass = SUCCESS_CLASS;
setTimeout(() => { setTimeout(() => {
this.buttonClass = PRIMARY_CLASS; this.buttonClass = PRIMARY_CLASS;

View file

@ -0,0 +1,41 @@
import { customElement } from "lit-element";
import Chart from "chart.js";
import { AdminApi, LoginMetrics } from "../../api";
import { AKChart } from "./Chart";
import { DEFAULT_CONFIG } from "../../api/Config";
@customElement("ak-charts-admin-login")
export class AdminLoginsChart extends AKChart<LoginMetrics> {
apiRequest(): Promise<LoginMetrics> {
return new AdminApi(DEFAULT_CONFIG).adminMetricsList();
}
getDatasets(data: LoginMetrics): Chart.ChartDataSets[] {
return [
{
label: "Failed Logins",
backgroundColor: "rgba(201, 25, 11, .5)",
spanGaps: true,
data: data.loginsFailedPer1h?.map((cord) => {
return {
x: cord.xCord,
y: cord.yCord,
};
}),
},
{
label: "Successful Logins",
backgroundColor: "rgba(189, 229, 184, .5)",
spanGaps: true,
data: data.loginsPer1h?.map((cord) => {
return {
x: cord.xCord,
y: cord.yCord,
};
}),
},
];
}
}

View file

@ -0,0 +1,32 @@
import { customElement, property } from "lit-element";
import { Coordinate, CoreApi } from "../../api";
import { DEFAULT_CONFIG } from "../../api/Config";
import { AKChart } from "./Chart";
@customElement("ak-charts-application-authorize")
export class ApplicationAuthorizeChart extends AKChart<Coordinate[]> {
@property()
applicationSlug!: string;
apiRequest(): Promise<Coordinate[]> {
return new CoreApi(DEFAULT_CONFIG).coreApplicationsMetrics({ slug: this.applicationSlug });
}
getDatasets(data: Coordinate[]): Chart.ChartDataSets[] {
return [
{
label: "Authorizations",
backgroundColor: "rgba(189, 229, 184, .5)",
spanGaps: true,
data: data.map((cord) => {
return {
x: cord.xCord,
y: cord.yCord,
};
}),
},
];
}
}

View file

@ -0,0 +1,103 @@
import { css, CSSResult, html, LitElement, TemplateResult } from "lit-element";
import Chart from "chart.js";
interface TickValue {
value: number;
major: boolean;
}
export abstract class AKChart<T> extends LitElement {
abstract apiRequest(): Promise<T>;
abstract getDatasets(data: T): Chart.ChartDataSets[];
chart?: Chart;
static get styles(): CSSResult[] {
return [css`
:host {
position: relative;
height: 100%;
width: 100%;
display: block;
min-height: 25rem;
}
canvas {
width: 100px;
height: 100px;
}
`];
}
constructor() {
super();
window.addEventListener("resize", () => {
if (this.chart) {
this.chart.resize();
}
});
}
configureChart(data: T, ctx: CanvasRenderingContext2D): Chart {
return new Chart(ctx, {
type: "bar",
data: {
datasets: this.getDatasets(data),
},
options: {
maintainAspectRatio: false,
spanGaps: true,
scales: {
xAxes: [
{
stacked: true,
gridLines: {
color: "rgba(0, 0, 0, 0)",
},
type: "time",
offset: true,
ticks: {
callback: function (value, index: number, values) {
const valueStamp = <TickValue>(<unknown>values[index]);
const delta = Date.now() - valueStamp.value;
const ago = Math.round(delta / 1000 / 3600);
return `${ago} Hours ago`;
},
autoSkip: true,
maxTicksLimit: 8,
},
},
],
yAxes: [
{
stacked: true,
gridLines: {
color: "rgba(0, 0, 0, 0)",
},
},
],
},
},
});
}
firstUpdated(): void {
this.apiRequest().then((r) => {
const canvas = <HTMLCanvasElement>this.shadowRoot?.querySelector("canvas");
if (!canvas) {
console.warn("Failed to get canvas element");
return false;
}
const ctx = canvas.getContext("2d");
if (!ctx) {
console.warn("failed to get 2d context");
return false;
}
this.chart = this.configureChart(r, ctx);
});
}
render(): TemplateResult {
return html`<canvas></canvas>`;
}
}

View file

@ -35,6 +35,7 @@ export class MessageContainer extends LitElement {
} }
connect(): void { connect(): void {
if (navigator.webdriver) return;
const wsUrl = `${window.location.protocol.replace("http", "ws")}//${ const wsUrl = `${window.location.protocol.replace("http", "ws")}//${
window.location.host window.location.host
}/ws/client/`; }/ws/client/`;

View file

@ -1,7 +1,8 @@
import { gettext } from "django"; import { gettext } from "django";
import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element"; import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
import { EventsApi, Notification } from "../../api";
import { AKResponse } from "../../api/Client"; import { AKResponse } from "../../api/Client";
import { Notification } from "../../api/EventNotification"; import { DEFAULT_CONFIG } from "../../api/Config";
import { COMMON_STYLES } from "../../common/styles"; import { COMMON_STYLES } from "../../common/styles";
@customElement("ak-notification-drawer") @customElement("ak-notification-drawer")
@ -30,9 +31,9 @@ export class NotificationDrawer extends LitElement {
} }
firstUpdated(): void { firstUpdated(): void {
Notification.list({ new EventsApi(DEFAULT_CONFIG).eventsNotificationsList({
seen: false, seen: "false",
ordering: "-created" ordering: "-created",
}).then(r => { }).then(r => {
this.notifications = r; this.notifications = r;
this.unread = r.results.length; this.unread = r.results.length;
@ -40,7 +41,6 @@ export class NotificationDrawer extends LitElement {
} }
renderItem(item: Notification): TemplateResult { renderItem(item: Notification): TemplateResult {
const created = new Date(parseInt(item.created, 10) * 1000);
let level = ""; let level = "";
switch (item.severity) { switch (item.severity) {
case "notice": case "notice":
@ -66,15 +66,18 @@ export class NotificationDrawer extends LitElement {
</div> </div>
<div class="pf-c-notification-drawer__list-item-action"> <div class="pf-c-notification-drawer__list-item-action">
<button class="pf-c-dropdown__toggle pf-m-plain" type="button" @click=${() => { <button class="pf-c-dropdown__toggle pf-m-plain" type="button" @click=${() => {
Notification.markSeen(item.pk).then(() => { new EventsApi(DEFAULT_CONFIG).eventsNotificationsPartialUpdate({
this.firstUpdated(); uuid: item.pk || "",
data: {
seen: true,
}
}); });
}}> }}>
<i class="fas fa-times"></i> <i class="fas fa-times"></i>
</button> </button>
</div> </div>
<p class="pf-c-notification-drawer__list-item-description">${item.body}</p> <p class="pf-c-notification-drawer__list-item-description">${item.body}</p>
<small class="pf-c-notification-drawer__list-item-timestamp">${created.toLocaleString()}</small> <small class="pf-c-notification-drawer__list-item-timestamp">${item.created?.toLocaleString()}</small>
</li>`; </li>`;
} }

View file

@ -2,16 +2,16 @@ import { gettext } from "django";
import { customElement, html, property, TemplateResult } from "lit-element"; import { customElement, html, property, TemplateResult } from "lit-element";
import { AKResponse } from "../../api/Client"; import { AKResponse } from "../../api/Client";
import { Table, TableColumn } from "../../elements/table/Table"; import { Table, TableColumn } from "../../elements/table/Table";
import { PolicyBinding } from "../../api/PolicyBindings"; import { PoliciesApi, PolicyBinding } from "../../api";
import "../../elements/Tabs"; import "../../elements/Tabs";
import "../../elements/AdminLoginsChart";
import "../../elements/buttons/ModalButton"; import "../../elements/buttons/ModalButton";
import "../../elements/buttons/SpinnerButton"; import "../../elements/buttons/SpinnerButton";
import "../../elements/buttons/Dropdown"; import "../../elements/buttons/Dropdown";
import { Policy } from "../../api/Policies";
import { until } from "lit-html/directives/until"; import { until } from "lit-html/directives/until";
import { PAGE_SIZE } from "../../constants"; import { PAGE_SIZE } from "../../constants";
import { DEFAULT_CONFIG } from "../../api/Config";
import { AdminURLManager } from "../../api/legacy";
@customElement("ak-bound-policies-list") @customElement("ak-bound-policies-list")
export class BoundPoliciesList extends Table<PolicyBinding> { export class BoundPoliciesList extends Table<PolicyBinding> {
@ -19,11 +19,11 @@ export class BoundPoliciesList extends Table<PolicyBinding> {
target?: string; target?: string;
apiEndpoint(page: number): Promise<AKResponse<PolicyBinding>> { apiEndpoint(page: number): Promise<AKResponse<PolicyBinding>> {
return PolicyBinding.list({ return new PoliciesApi(DEFAULT_CONFIG).policiesBindingsList({
target: this.target || "", target: this.target || "",
ordering: "order", ordering: "order",
page: page, page: page,
page_size: PAGE_SIZE, pageSize: PAGE_SIZE,
}); });
} }
@ -56,13 +56,13 @@ export class BoundPoliciesList extends Table<PolicyBinding> {
html`${item.order}`, html`${item.order}`,
html`${item.timeout}`, html`${item.timeout}`,
html` html`
<ak-modal-button href="${PolicyBinding.adminUrl(`${item.pk}/update/`)}"> <ak-modal-button href="${AdminURLManager.policyBindings(`${item.pk}/update/`)}">
<ak-spinner-button slot="trigger" class="pf-m-secondary"> <ak-spinner-button slot="trigger" class="pf-m-secondary">
${gettext("Edit")} ${gettext("Edit")}
</ak-spinner-button> </ak-spinner-button>
<div slot="modal"></div> <div slot="modal"></div>
</ak-modal-button> </ak-modal-button>
<ak-modal-button href="${PolicyBinding.adminUrl(`${item.pk}/delete/`)}"> <ak-modal-button href="${AdminURLManager.policyBindings(`${item.pk}/delete/`)}">
<ak-spinner-button slot="trigger" class="pf-m-danger"> <ak-spinner-button slot="trigger" class="pf-m-danger">
${gettext("Delete")} ${gettext("Delete")}
</ak-spinner-button> </ak-spinner-button>
@ -78,7 +78,7 @@ export class BoundPoliciesList extends Table<PolicyBinding> {
${gettext("No policies are currently bound to this object.")} ${gettext("No policies are currently bound to this object.")}
</div> </div>
<div slot="primary"> <div slot="primary">
<ak-modal-button href=${PolicyBinding.adminUrl(`create/?target=${this.target}`)}> <ak-modal-button href=${AdminURLManager.policyBindings(`create/?target=${this.target}`)}>
<ak-spinner-button slot="trigger" class="pf-m-primary"> <ak-spinner-button slot="trigger" class="pf-m-primary">
${gettext("Bind Policy")} ${gettext("Bind Policy")}
</ak-spinner-button> </ak-spinner-button>
@ -96,7 +96,7 @@ export class BoundPoliciesList extends Table<PolicyBinding> {
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i> <i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
</button> </button>
<ul class="pf-c-dropdown__menu" hidden> <ul class="pf-c-dropdown__menu" hidden>
${until(Policy.getTypes().then((types) => { ${until(new PoliciesApi(DEFAULT_CONFIG).policiesAllTypes({}).then((types) => {
return types.map((type) => { return types.map((type) => {
return html`<li> return html`<li>
<ak-modal-button href="${type.link}"> <ak-modal-button href="${type.link}">
@ -110,7 +110,7 @@ export class BoundPoliciesList extends Table<PolicyBinding> {
}), html`<ak-spinner></ak-spinner>`)} }), html`<ak-spinner></ak-spinner>`)}
</ul> </ul>
</ak-dropdown> </ak-dropdown>
<ak-modal-button href=${PolicyBinding.adminUrl(`create/?target=${this.target}`)}> <ak-modal-button href=${AdminURLManager.policyBindings(`create/?target=${this.target}`)}>
<ak-spinner-button slot="trigger" class="pf-m-primary"> <ak-spinner-button slot="trigger" class="pf-m-primary">
${gettext("Bind Policy")} ${gettext("Bind Policy")}
</ak-spinner-button> </ak-spinner-button>

View file

@ -3,15 +3,17 @@ import { css, CSSResult, customElement, html, LitElement, property, TemplateResu
import PageStyle from "@patternfly/patternfly/components/Page/page.css"; import PageStyle from "@patternfly/patternfly/components/Page/page.css";
// @ts-ignore // @ts-ignore
import GlobalsStyle from "@patternfly/patternfly/base/patternfly-globals.css"; import GlobalsStyle from "@patternfly/patternfly/base/patternfly-globals.css";
import { Config } from "../../api/Config"; import { configureSentry } from "../../api/Config";
import { Config } from "../../api";
import { ifDefined } from "lit-html/directives/if-defined";
export const DefaultConfig: Config = { export const DefaultConfig: Config = {
branding_logo: " /static/dist/assets/icons/icon_left_brand.svg", brandingLogo: " /static/dist/assets/icons/icon_left_brand.svg",
branding_title: "authentik", brandingTitle: "authentik",
error_reporting_enabled: false, errorReportingEnabled: false,
error_reporting_environment: "", errorReportingEnvironment: "",
error_reporting_send_pii: false, errorReportingSendPii: false,
}; };
@customElement("ak-sidebar-brand") @customElement("ak-sidebar-brand")
@ -40,13 +42,13 @@ export class SidebarBrand extends LitElement {
} }
firstUpdated(): void { firstUpdated(): void {
Config.get().then((c) => (this.config = c)); configureSentry().then((c) => {this.config = c;});
} }
render(): TemplateResult { render(): TemplateResult {
return html` <a href="#/" class="pf-c-page__header-brand-link"> return html` <a href="#/" class="pf-c-page__header-brand-link">
<div class="pf-c-brand ak-brand"> <div class="pf-c-brand ak-brand">
<img src="${this.config.branding_logo}" alt="authentik icon" loading="lazy" /> <img src="${ifDefined(this.config.brandingLogo)}" alt="authentik icon" loading="lazy" />
</div> </div>
</a>`; </a>`;
} }

View file

@ -5,10 +5,11 @@ import NavStyle from "@patternfly/patternfly/components/Nav/nav.css";
import fa from "@fortawesome/fontawesome-free/css/all.css"; import fa from "@fortawesome/fontawesome-free/css/all.css";
// @ts-ignore // @ts-ignore
import AvatarStyle from "@patternfly/patternfly/components/Avatar/avatar.css"; import AvatarStyle from "@patternfly/patternfly/components/Avatar/avatar.css";
import { User } from "../../api/Users"; import { me } from "../../api/Users";
import { until } from "lit-html/directives/until"; import { until } from "lit-html/directives/until";
import "../notifications/NotificationTrigger"; import "../notifications/NotificationTrigger";
import { ifDefined } from "lit-html/directives/if-defined";
@customElement("ak-sidebar-user") @customElement("ak-sidebar-user")
export class SidebarUser extends LitElement { export class SidebarUser extends LitElement {
@ -37,8 +38,8 @@ export class SidebarUser extends LitElement {
render(): TemplateResult { render(): TemplateResult {
return html` return html`
<a href="#/-/user/" class="pf-c-nav__link user-avatar" id="user-settings"> <a href="#/-/user/" class="pf-c-nav__link user-avatar" id="user-settings">
${until(User.me().then((u) => { ${until(me().then((u) => {
return html`<img class="pf-c-avatar" src="${u.avatar}" alt="" />`; return html`<img class="pf-c-avatar" src="${ifDefined(u.avatar)}" alt="" />`;
}), html``)} }), html``)}
</a> </a>
<ak-notification-trigger class="pf-c-nav__link user-notifications"> <ak-notification-trigger class="pf-c-nav__link user-notifications">

View file

@ -5,7 +5,7 @@ import { COMMON_STYLES } from "../../common/styles";
import "./TablePagination"; import "./TablePagination";
import "../EmptyState"; import "../EmptyState";
import "../Spinner";
export class TableColumn { export class TableColumn {

View file

@ -1,12 +1,12 @@
import { CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element"; import { CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
import { COMMON_STYLES } from "../../common/styles"; import { COMMON_STYLES } from "../../common/styles";
import { PBPagination } from "../../api/Client"; import { AKPagination } from "../../api/Client";
import { gettext } from "django"; import { gettext } from "django";
@customElement("ak-table-pagination") @customElement("ak-table-pagination")
export class TablePagination extends LitElement { export class TablePagination extends LitElement {
@property({attribute: false}) @property({attribute: false})
pages?: PBPagination; pages?: AKPagination;
@property({attribute: false}) @property({attribute: false})
// eslint-disable-next-line // eslint-disable-next-line
@ -22,8 +22,8 @@ export class TablePagination extends LitElement {
<div class="pf-c-options-menu"> <div class="pf-c-options-menu">
<div class="pf-c-options-menu__toggle pf-m-text pf-m-plain"> <div class="pf-c-options-menu__toggle pf-m-text pf-m-plain">
<span class="pf-c-options-menu__toggle-text"> <span class="pf-c-options-menu__toggle-text">
${this.pages?.start_index} - ${this.pages?.startIndex} -
${this.pages?.end_index} of ${this.pages?.endIndex} of
${this.pages?.count} ${this.pages?.count}
</span> </span>
</div> </div>

View file

@ -1,3 +1,3 @@
import "construct-style-sheets-polyfill"; import "construct-style-sheets-polyfill";
import "./pages/generic/FlowExecutor"; import "./flows/FlowExecutor";

View file

@ -1,34 +1,34 @@
import { gettext } from "django"; import { gettext } from "django";
import { LitElement, html, customElement, property, TemplateResult, CSSResult, css } from "lit-element"; import { LitElement, html, customElement, property, TemplateResult, CSSResult, css } from "lit-element";
import { unsafeHTML } from "lit-html/directives/unsafe-html"; import { unsafeHTML } from "lit-html/directives/unsafe-html";
import { getCookie } from "../../utils"; import "./stages/authenticator_static/AuthenticatorStaticStage";
import "../../elements/stages/authenticator_static/AuthenticatorStaticStage"; import "./stages/authenticator_totp/AuthenticatorTOTPStage";
import "../../elements/stages/authenticator_totp/AuthenticatorTOTPStage"; import "./stages/authenticator_validate/AuthenticatorValidateStage";
import "../../elements/stages/authenticator_validate/AuthenticatorValidateStage"; import "./stages/authenticator_webauthn/WebAuthnAuthenticatorRegisterStage";
import "../../elements/stages/authenticator_webauthn/WebAuthnAuthenticatorRegisterStage"; import "./stages/autosubmit/AutosubmitStage";
import "../../elements/stages/autosubmit/AutosubmitStage"; import "./stages/captcha/CaptchaStage";
import "../../elements/stages/captcha/CaptchaStage"; import "./stages/consent/ConsentStage";
import "../../elements/stages/consent/ConsentStage"; import "./stages/email/EmailStage";
import "../../elements/stages/email/EmailStage"; import "./stages/identification/IdentificationStage";
import "../../elements/stages/identification/IdentificationStage"; import "./stages/password/PasswordStage";
import "../../elements/stages/password/PasswordStage"; import "./stages/prompt/PromptStage";
import "../../elements/stages/prompt/PromptStage"; import { ShellChallenge, RedirectChallenge } from "../api/Flows";
import { ShellChallenge, Challenge, ChallengeTypes, Flow, RedirectChallenge } from "../../api/Flows"; import { IdentificationChallenge } from "./stages/identification/IdentificationStage";
import { DefaultClient } from "../../api/Client"; import { PasswordChallenge } from "./stages/password/PasswordStage";
import { IdentificationChallenge } from "../../elements/stages/identification/IdentificationStage"; import { ConsentChallenge } from "./stages/consent/ConsentStage";
import { PasswordChallenge } from "../../elements/stages/password/PasswordStage"; import { EmailChallenge } from "./stages/email/EmailStage";
import { ConsentChallenge } from "../../elements/stages/consent/ConsentStage"; import { AutosubmitChallenge } from "./stages/autosubmit/AutosubmitStage";
import { EmailChallenge } from "../../elements/stages/email/EmailStage"; import { PromptChallenge } from "./stages/prompt/PromptStage";
import { AutosubmitChallenge } from "../../elements/stages/autosubmit/AutosubmitStage"; import { AuthenticatorTOTPChallenge } from "./stages/authenticator_totp/AuthenticatorTOTPStage";
import { PromptChallenge } from "../../elements/stages/prompt/PromptStage"; import { AuthenticatorStaticChallenge } from "./stages/authenticator_static/AuthenticatorStaticStage";
import { AuthenticatorTOTPChallenge } from "../../elements/stages/authenticator_totp/AuthenticatorTOTPStage"; import { AuthenticatorValidateStageChallenge } from "./stages/authenticator_validate/AuthenticatorValidateStage";
import { AuthenticatorStaticChallenge } from "../../elements/stages/authenticator_static/AuthenticatorStaticStage"; import { WebAuthnAuthenticatorRegisterChallenge } from "./stages/authenticator_webauthn/WebAuthnAuthenticatorRegisterStage";
import { AuthenticatorValidateStageChallenge } from "../../elements/stages/authenticator_validate/AuthenticatorValidateStage"; import { CaptchaChallenge } from "./stages/captcha/CaptchaStage";
import { WebAuthnAuthenticatorRegisterChallenge } from "../../elements/stages/authenticator_webauthn/WebAuthnAuthenticatorRegisterStage"; import { COMMON_STYLES } from "../common/styles";
import { CaptchaChallenge } from "../../elements/stages/captcha/CaptchaStage"; import { SpinnerSize } from "../elements/Spinner";
import { COMMON_STYLES } from "../../common/styles"; import { StageHost } from "./stages/base";
import { SpinnerSize } from "../../elements/Spinner"; import { Challenge, ChallengeTypeEnum, FlowsApi } from "../api";
import { StageHost } from "../../elements/stages/base"; import { DEFAULT_CONFIG } from "../api/Config";
@customElement("ak-flow-executor") @customElement("ak-flow-executor")
export class FlowExecutor extends LitElement implements StageHost { export class FlowExecutor extends LitElement implements StageHost {
@ -68,37 +68,30 @@ export class FlowExecutor extends LitElement implements StageHost {
}); });
} }
submit(formData?: FormData): Promise<void> { submit<T>(formData?: T): Promise<void> {
const csrftoken = getCookie("authentik_csrf");
const request = new Request(DefaultClient.makeUrl(["flows", "executor", this.flowSlug]), {
headers: {
"X-CSRFToken": csrftoken,
},
});
this.loading = true; this.loading = true;
return fetch(request, { return new FlowsApi(DEFAULT_CONFIG).flowsExecutorSolveRaw({
method: "POST", flowSlug: this.flowSlug,
mode: "same-origin", data: formData || {},
body: formData, }).then((challengeRaw) => {
}) return challengeRaw.raw.json();
.then((response) => { }).then((data) => {
return response.json(); this.challenge = data;
}) }).catch((e) => {
.then((data) => { this.errorMessage(e);
this.challenge = data; }).finally(() => {
}) this.loading = false;
.catch((e) => { });
this.errorMessage(e);
})
.finally(() => {
this.loading = false;
});
} }
firstUpdated(): void { firstUpdated(): void {
this.loading = true; this.loading = true;
Flow.executor(this.flowSlug).then((challenge) => { new FlowsApi(DEFAULT_CONFIG).flowsExecutorGetRaw({
this.challenge = challenge; flowSlug: this.flowSlug
}).then((challengeRaw) => {
return challengeRaw.raw.json();
}).then((challenge) => {
this.challenge = challenge as Challenge;
}).catch((e) => { }).catch((e) => {
// Catch JSON or Update errors // Catch JSON or Update errors
this.errorMessage(e); this.errorMessage(e);
@ -109,7 +102,7 @@ export class FlowExecutor extends LitElement implements StageHost {
errorMessage(error: string): void { errorMessage(error: string): void {
this.challenge = <ShellChallenge>{ this.challenge = <ShellChallenge>{
type: ChallengeTypes.shell, type: ChallengeTypeEnum.Shell,
body: `<style> body: `<style>
.ak-exception { .ak-exception {
font-family: monospace; font-family: monospace;
@ -139,13 +132,13 @@ export class FlowExecutor extends LitElement implements StageHost {
return this.renderLoading(); return this.renderLoading();
} }
switch (this.challenge.type) { switch (this.challenge.type) {
case ChallengeTypes.redirect: case ChallengeTypeEnum.Redirect:
console.debug(`authentik/flows: redirecting to ${(this.challenge as RedirectChallenge).to}`); console.debug(`authentik/flows: redirecting to ${(this.challenge as RedirectChallenge).to}`);
window.location.assign((this.challenge as RedirectChallenge).to); window.location.assign((this.challenge as RedirectChallenge).to);
return this.renderLoading(); return this.renderLoading();
case ChallengeTypes.shell: case ChallengeTypeEnum.Shell:
return html`${unsafeHTML((this.challenge as ShellChallenge).body)}`; return html`${unsafeHTML((this.challenge as ShellChallenge).body)}`;
case ChallengeTypes.native: case ChallengeTypeEnum.Native:
switch (this.challenge.component) { switch (this.challenge.component) {
case "ak-stage-identification": case "ak-stage-identification":
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>`;

View file

@ -4,6 +4,7 @@ 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";
import "../form"; import "../form";
import "../../../elements/utils/LoadingState";
export interface AuthenticatorStaticChallenge extends WithUserInfoChallenge { export interface AuthenticatorStaticChallenge extends WithUserInfoChallenge {
codes: number[]; codes: number[];

View file

@ -5,7 +5,8 @@ import { COMMON_STYLES } from "../../../common/styles";
import { BaseStage } from "../base"; import { BaseStage } from "../base";
import "webcomponent-qr-code"; import "webcomponent-qr-code";
import "../form"; import "../form";
import { showMessage } from "../../messages/MessageContainer"; import { showMessage } from "../../../elements/messages/MessageContainer";
import "../../../elements/utils/LoadingState";
export interface AuthenticatorTOTPChallenge extends WithUserInfoChallenge { export interface AuthenticatorTOTPChallenge extends WithUserInfoChallenge {
config_url: string; config_url: string;

View file

@ -36,8 +36,8 @@ export class AuthenticatorValidateStage extends BaseStage implements StageHost {
@property({attribute: false}) @property({attribute: false})
selectedDeviceChallenge?: DeviceChallenge; selectedDeviceChallenge?: DeviceChallenge;
submit(formData?: FormData): Promise<void> { submit<T>(formData?: T): Promise<void> {
return this.host?.submit(formData) || Promise.resolve(); return this.host?.submit<T>(formData) || Promise.resolve();
} }
static get styles(): CSSResult[] { static get styles(): CSSResult[] {

View file

@ -4,6 +4,7 @@ import { COMMON_STYLES } from "../../../common/styles";
import { BaseStage } from "../base"; import { BaseStage } from "../base";
import { AuthenticatorValidateStage, AuthenticatorValidateStageChallenge, DeviceChallenge } from "./AuthenticatorValidateStage"; import { AuthenticatorValidateStage, AuthenticatorValidateStageChallenge, DeviceChallenge } from "./AuthenticatorValidateStage";
import "../form"; import "../form";
import "../../../elements/utils/LoadingState";
@customElement("ak-stage-authenticator-validate-code") @customElement("ak-stage-authenticator-validate-code")
export class AuthenticatorValidateStageWebCode extends BaseStage { export class AuthenticatorValidateStageWebCode extends BaseStage {

View file

@ -1,7 +1,7 @@
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 { COMMON_STYLES } from "../../../common/styles"; import { COMMON_STYLES } from "../../../common/styles";
import { SpinnerSize } from "../../Spinner"; import { SpinnerSize } from "../../../elements/Spinner";
import { transformAssertionForServer, transformCredentialRequestOptions } from "../authenticator_webauthn/utils"; import { transformAssertionForServer, transformCredentialRequestOptions } from "../authenticator_webauthn/utils";
import { BaseStage } from "../base"; import { BaseStage } from "../base";
import { AuthenticatorValidateStage, AuthenticatorValidateStageChallenge, DeviceChallenge } from "./AuthenticatorValidateStage"; import { AuthenticatorValidateStage, AuthenticatorValidateStageChallenge, DeviceChallenge } from "./AuthenticatorValidateStage";

View file

@ -1,7 +1,7 @@
import { gettext } from "django"; import { gettext } from "django";
import { customElement, html, property, TemplateResult } from "lit-element"; import { customElement, html, property, TemplateResult } from "lit-element";
import { WithUserInfoChallenge } from "../../../api/Flows"; import { WithUserInfoChallenge } from "../../../api/Flows";
import { SpinnerSize } from "../../Spinner"; import { SpinnerSize } from "../../../elements/Spinner";
import { BaseStage } from "../base"; import { BaseStage } from "../base";
import { Assertion, transformCredentialCreateOptions, transformNewAssertionForServer } from "./utils"; import { Assertion, transformCredentialCreateOptions, transformNewAssertionForServer } from "./utils";

View file

@ -3,7 +3,8 @@ import { CSSResult, customElement, html, property, TemplateResult } from "lit-el
import { WithUserInfoChallenge } 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";
import "../../Spinner"; import "../../../elements/Spinner";
import "../../../elements/utils/LoadingState";
export interface AutosubmitChallenge extends WithUserInfoChallenge { export interface AutosubmitChallenge extends WithUserInfoChallenge {
url: string; url: string;

View file

@ -1,7 +1,7 @@
import { LitElement } from "lit-element"; import { LitElement } from "lit-element";
export interface StageHost { export interface StageHost {
submit(formData?: FormData): Promise<void>; submit<T>(formData?: T): Promise<void>;
} }
export class BaseStage extends LitElement { export class BaseStage extends LitElement {
@ -10,8 +10,12 @@ export class BaseStage extends LitElement {
submitForm(e: Event): void { submitForm(e: Event): void {
e.preventDefault(); e.preventDefault();
const object: {
[key: string]: unknown;
} = {};
const form = new FormData(this.shadowRoot?.querySelector("form") || undefined); const form = new FormData(this.shadowRoot?.querySelector("form") || undefined);
this.host?.submit(form); form.forEach((value, key) => object[key] = value);
this.host?.submit(object);
} }
} }

View file

@ -2,9 +2,10 @@ import { gettext } from "django";
import { CSSResult, customElement, html, property, TemplateResult } from "lit-element"; import { CSSResult, customElement, html, property, TemplateResult } from "lit-element";
import { WithUserInfoChallenge } from "../../../api/Flows"; import { WithUserInfoChallenge } from "../../../api/Flows";
import { COMMON_STYLES } from "../../../common/styles"; import { COMMON_STYLES } from "../../../common/styles";
import { SpinnerSize } from "../../Spinner"; import { SpinnerSize } from "../../../elements/Spinner";
import { BaseStage } from "../base"; import { BaseStage } from "../base";
import "../form"; import "../form";
import "../../../elements/utils/LoadingState";
export interface CaptchaChallenge extends WithUserInfoChallenge { export interface CaptchaChallenge extends WithUserInfoChallenge {
site_key: string; site_key: string;

View file

@ -3,6 +3,7 @@ import { CSSResult, customElement, html, property, TemplateResult } from "lit-el
import { WithUserInfoChallenge } 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";
import "../../../elements/utils/LoadingState";
export interface Permission { export interface Permission {
name: string; name: string;

View file

@ -1,10 +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 { Challenge } from "../../../api";
import { COMMON_STYLES } from "../../../common/styles"; import { COMMON_STYLES } from "../../../common/styles";
import { BaseStage } from "../base"; import { BaseStage } from "../base";
import "../../../elements/utils/LoadingState";
export type EmailChallenge = Challenge export type EmailChallenge = Challenge;
@customElement("ak-stage-email") @customElement("ak-stage-email")
export class EmailStage extends BaseStage { export class EmailStage extends BaseStage {

View file

@ -1,9 +1,10 @@
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 { COMMON_STYLES } from "../../../common/styles"; import { COMMON_STYLES } from "../../../common/styles";
import { BaseStage } from "../base"; import { BaseStage } from "../base";
import "../form"; import "../form";
import "../../../elements/utils/LoadingState";
import { Challenge } from "../../../api/Flows";
export interface IdentificationChallenge extends Challenge { export interface IdentificationChallenge extends Challenge {

View file

@ -4,6 +4,7 @@ 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";
import "../form"; import "../form";
import "../../../elements/utils/LoadingState";
export interface PasswordChallenge extends WithUserInfoChallenge { export interface PasswordChallenge extends WithUserInfoChallenge {
recovery_url?: string; recovery_url?: string;

View file

@ -1,10 +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 { unsafeHTML } from "lit-html/directives/unsafe-html"; import { unsafeHTML } from "lit-html/directives/unsafe-html";
import { Challenge } from "../../../api/Flows";
import { COMMON_STYLES } from "../../../common/styles"; import { COMMON_STYLES } from "../../../common/styles";
import { BaseStage } from "../base"; import { BaseStage } from "../base";
import "../form"; import "../form";
import "../../../elements/utils/LoadingState";
import { Challenge } from "../../../api/Flows";
export interface Prompt { export interface Prompt {
field_key: string; field_key: string;

View file

@ -1,5 +1,5 @@
import { customElement } from "lit-element"; import { customElement } from "lit-element";
import { User } from "../api/Users"; import { me } from "../api/Users";
import { SidebarItem } from "../elements/sidebar/Sidebar"; import { SidebarItem } from "../elements/sidebar/Sidebar";
import { ID_REGEX, SLUG_REGEX, UUID_REGEX } from "../elements/router/Route"; import { ID_REGEX, SLUG_REGEX, UUID_REGEX } from "../elements/router/Route";
import { Interface } from "./Interface"; import { Interface } from "./Interface";
@ -10,7 +10,7 @@ export const SIDEBAR_ITEMS: SidebarItem[] = [
new SidebarItem("Overview", "/administration/overview"), new SidebarItem("Overview", "/administration/overview"),
new SidebarItem("System Tasks", "/administration/system-tasks"), new SidebarItem("System Tasks", "/administration/system-tasks"),
).when((): Promise<boolean> => { ).when((): Promise<boolean> => {
return User.me().then(u => u.is_superuser); return me().then(u => u.isSuperuser||false);
}), }),
new SidebarItem("Events").children( new SidebarItem("Events").children(
new SidebarItem("Log", "/events/log").activeWhen( new SidebarItem("Log", "/events/log").activeWhen(
@ -19,7 +19,7 @@ export const SIDEBAR_ITEMS: SidebarItem[] = [
new SidebarItem("Notification Rules", "/events/rules"), new SidebarItem("Notification Rules", "/events/rules"),
new SidebarItem("Notification Transports", "/events/transports"), new SidebarItem("Notification Transports", "/events/transports"),
).when((): Promise<boolean> => { ).when((): Promise<boolean> => {
return User.me().then(u => u.is_superuser); return me().then(u => u.isSuperuser||false);
}), }),
new SidebarItem("Resources").children( new SidebarItem("Resources").children(
new SidebarItem("Applications", "/core/applications").activeWhen( new SidebarItem("Applications", "/core/applications").activeWhen(
@ -34,13 +34,13 @@ export const SIDEBAR_ITEMS: SidebarItem[] = [
new SidebarItem("Outposts", "/outpost/outposts"), new SidebarItem("Outposts", "/outpost/outposts"),
new SidebarItem("Outpost Service Connections", "/outpost/service-connections"), new SidebarItem("Outpost Service Connections", "/outpost/service-connections"),
).when((): Promise<boolean> => { ).when((): Promise<boolean> => {
return User.me().then(u => u.is_superuser); return me().then(u => u.isSuperuser||false);
}), }),
new SidebarItem("Customisation").children( new SidebarItem("Customisation").children(
new SidebarItem("Policies", "/policy/policies"), new SidebarItem("Policies", "/policy/policies"),
new SidebarItem("Property Mappings", "/core/property-mappings"), new SidebarItem("Property Mappings", "/core/property-mappings"),
).when((): Promise<boolean> => { ).when((): Promise<boolean> => {
return User.me().then(u => u.is_superuser); return me().then(u => u.isSuperuser||false);
}), }),
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})$`),
@ -48,7 +48,7 @@ export const SIDEBAR_ITEMS: SidebarItem[] = [
new SidebarItem("Prompts", "/flow/stages/prompts"), new SidebarItem("Prompts", "/flow/stages/prompts"),
new SidebarItem("Invitations", "/flow/stages/invitations"), new SidebarItem("Invitations", "/flow/stages/invitations"),
).when((): Promise<boolean> => { ).when((): Promise<boolean> => {
return User.me().then(u => u.is_superuser); return me().then(u => u.isSuperuser||false);
}), }),
new SidebarItem("Identity & Cryptography").children( new SidebarItem("Identity & Cryptography").children(
new SidebarItem("User", "/identity/users"), new SidebarItem("User", "/identity/users"),
@ -56,7 +56,7 @@ export const SIDEBAR_ITEMS: SidebarItem[] = [
new SidebarItem("Certificates", "/crypto/certificates"), new SidebarItem("Certificates", "/crypto/certificates"),
new SidebarItem("Tokens", "/core/tokens"), new SidebarItem("Tokens", "/core/tokens"),
).when((): Promise<boolean> => { ).when((): Promise<boolean> => {
return User.me().then(u => u.is_superuser); return me().then(u => u.isSuperuser||false);
}), }),
]; ];

View file

@ -1,34 +1,13 @@
import "construct-style-sheets-polyfill"; import "construct-style-sheets-polyfill";
// Elements that are used by SiteShell pages
// And can't dynamically be imported
import "./elements/buttons/ActionButton"; import "./elements/buttons/ActionButton";
import "./elements/buttons/Dropdown"; import "./elements/buttons/Dropdown";
import "./elements/buttons/ModalButton"; import "./elements/buttons/ModalButton";
import "./elements/buttons/SpinnerButton"; import "./elements/buttons/SpinnerButton";
import "./elements/buttons/TokenCopyButton";
import "./elements/sidebar/Sidebar";
import "./elements/sidebar/SidebarBrand";
import "./elements/sidebar/SidebarUser";
import "./elements/table/TablePagination";
import "./elements/AdminLoginsChart";
import "./elements/EmptyState";
import "./elements/cards/AggregateCard";
import "./elements/cards/AggregatePromiseCard";
import "./elements/CodeMirror"; import "./elements/CodeMirror";
import "./elements/messages/MessageContainer";
import "./elements/Spinner";
import "./elements/Tabs";
import "./elements/router/RouterOutlet";
import "./pages/generic/SiteShell";
import "./pages/admin-overview/AdminOverviewPage";
import "./pages/admin-overview/TopApplicationsTable";
import "./pages/applications/ApplicationListPage";
import "./pages/applications/ApplicationViewPage";
import "./pages/tokens/UserTokenList"; import "./pages/tokens/UserTokenList";
import "./pages/LibraryPage"; import "./pages/generic/SiteShell";
import "./interfaces/AdminInterface"; import "./interfaces/AdminInterface";

View file

@ -1,8 +1,9 @@
import { gettext } from "django"; import { gettext } from "django";
import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element"; import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
import { ifDefined } from "lit-html/directives/if-defined"; import { ifDefined } from "lit-html/directives/if-defined";
import { Application } from "../api/Applications"; import { Application, CoreApi } from "../api";
import { AKResponse } from "../api/Client"; import { AKResponse } from "../api/Client";
import { DEFAULT_CONFIG } from "../api/Config";
import { COMMON_STYLES } from "../common/styles"; import { COMMON_STYLES } from "../common/styles";
import { loading, truncate } from "../utils"; import { loading, truncate } from "../utils";
@ -31,19 +32,19 @@ export class LibraryApplication extends LitElement {
if (!this.application) { if (!this.application) {
return html`<ak-spinner></ak-spinner>`; return html`<ak-spinner></ak-spinner>`;
} }
return html` <a href="${this.application.launch_url}" class="pf-c-card pf-m-hoverable pf-m-compact"> return html` <a href="${ifDefined(this.application.launchUrl)}" class="pf-c-card pf-m-hoverable pf-m-compact">
<div class="pf-c-card__header"> <div class="pf-c-card__header">
${this.application.meta_icon ${this.application.metaIcon
? html`<img class="app-icon pf-c-avatar" src="${ifDefined(this.application.meta_icon)}" alt="Application Icon"/>` ? html`<img class="app-icon pf-c-avatar" src="${ifDefined(this.application.metaIcon)}" alt="Application Icon"/>`
: html`<i class="pf-icon pf-icon-arrow"></i>`} : html`<i class="pf-icon pf-icon-arrow"></i>`}
</div> </div>
<div class="pf-c-card__title"> <div class="pf-c-card__title">
<p id="card-1-check-label">${this.application.name}</p> <p id="card-1-check-label">${this.application.name}</p>
<div class="pf-c-content"> <div class="pf-c-content">
<small>${this.application.meta_publisher}</small> <small>${this.application.metaPublisher}</small>
</div> </div>
</div> </div>
<div class="pf-c-card__body">${truncate(this.application.meta_description, 35)}</div> <div class="pf-c-card__body">${truncate(this.application.metaDescription, 35)}</div>
</a>`; </a>`;
} }
@ -64,7 +65,9 @@ export class LibraryPage extends LitElement {
} }
firstUpdated(): void { firstUpdated(): void {
Application.list().then((r) => (this.apps = r)); new CoreApi(DEFAULT_CONFIG).coreApplicationsList({}).then((apps) => {
this.apps = apps;
});
} }
renderEmptyState(): TemplateResult { renderEmptyState(): TemplateResult {

View file

@ -2,7 +2,7 @@ import { gettext } from "django";
import { CSSResult, customElement, html, LitElement, TemplateResult } from "lit-element"; import { CSSResult, customElement, html, LitElement, TemplateResult } from "lit-element";
import { COMMON_STYLES } from "../../common/styles"; import { COMMON_STYLES } from "../../common/styles";
import "../../elements/AdminLoginsChart"; import "../../elements/charts/AdminLoginsChart";
import "../../elements/cards/AggregatePromiseCard"; import "../../elements/cards/AggregatePromiseCard";
import "./TopApplicationsTable"; import "./TopApplicationsTable";
import "./cards/AdminStatusCard"; import "./cards/AdminStatusCard";
@ -30,7 +30,7 @@ export class AdminOverviewPage extends LitElement {
<section class="pf-c-page__main-section"> <section class="pf-c-page__main-section">
<div class="pf-l-gallery pf-m-gutter"> <div class="pf-l-gallery pf-m-gutter">
<ak-aggregate-card class="pf-l-gallery__item pf-m-4-col" icon="pf-icon pf-icon-server" header="Logins over the last 24 hours" style="grid-column-end: span 3;grid-row-end: span 2;"> <ak-aggregate-card class="pf-l-gallery__item pf-m-4-col" icon="pf-icon pf-icon-server" header="Logins over the last 24 hours" style="grid-column-end: span 3;grid-row-end: span 2;">
<ak-admin-logins-chart .url="${["admin", "metrics"]}"></ak-admin-logins-chart> <ak-charts-admin-login></ak-charts-admin-login>
</ak-aggregate-card> </ak-aggregate-card>
<ak-aggregate-card class="pf-l-gallery__item pf-m-4-col" icon="pf-icon pf-icon-server" header="Apps with most usage" style="grid-column-end: span 2;grid-row-end: span 3;"> <ak-aggregate-card class="pf-l-gallery__item pf-m-4-col" icon="pf-icon pf-icon-server" header="Apps with most usage" style="grid-column-end: span 2;grid-row-end: span 3;">
<ak-top-applications-table></ak-top-applications-table> <ak-top-applications-table></ak-top-applications-table>

Some files were not shown because too many files have changed in this diff Show more