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
steps:
- 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
env:
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}

View File

@ -45,4 +45,5 @@ COPY ./lifecycle/ /lifecycle
USER authentik
STOPSIGNAL SIGINT
ENV TMPDIR /dev/shm/
ENV PYTHONUBUFFERED 1
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.functions import ExtractHour
from django.utils.timezone import now
from drf_yasg2.utils import swagger_auto_schema
from rest_framework.fields import SerializerMethodField
from drf_yasg2.utils import swagger_auto_schema, swagger_serializer_method
from rest_framework.fields import IntegerField, SerializerMethodField
from rest_framework.permissions import IsAdminUser
from rest_framework.request import Request
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):
results.append(
{
"x": time.mktime((_now + timedelta(hours=hour)).timetuple()) * 1000,
"y": data[hour * -1],
"x_cord": time.mktime((_now + timedelta(hours=hour)).timetuple())
* 1000,
"y_cord": data[hour * -1],
}
)
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"""
logins_per_1h = SerializerMethodField()
logins_failed_per_1h = SerializerMethodField()
@swagger_serializer_method(serializer_or_field=CoordinateSerializer(many=True))
def get_logins_per_1h(self, _):
"""Get successful logins per hour for the last 24 hours"""
return get_events_per_1h(action=EventAction.LOGIN)
@swagger_serializer_method(serializer_or_field=CoordinateSerializer(many=True))
def get_logins_failed_per_1h(self, _):
"""Get failed logins per hour for the last 24 hours"""
return get_events_per_1h(action=EventAction.LOGIN_FAILED)
@ -70,8 +86,8 @@ class AdministrationMetricsViewSet(ViewSet):
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:
"""Login Metrics per 1h"""
serializer = AdministrationMetricsSerializer(True)
serializer = LoginMetricsSerializer(True)
return Response(serializer.data)

View File

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

View File

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

View File

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

View File

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

View File

@ -16,6 +16,16 @@
{% block body %}
<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>
<ak-message-container></ak-message-container>
<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.http.response import HttpResponse
from django.urls import reverse_lazy
from django.utils.translation import gettext as _
from django.views.generic import UpdateView
from django.views.generic.base import TemplateView
@ -34,7 +35,7 @@ class UserDetailsView(SuccessMessageMixin, LoginRequiredMixin, UpdateView):
form_class = UserDetailForm
success_message = _("Successfully updated user.")
success_url = "/"
success_url = reverse_lazy("authentik_core:user-details")
def get_object(self):
return self.request.user

View File

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

View File

@ -1,11 +1,12 @@
"""NotificationTransport API Views"""
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 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.response import Response
from rest_framework.serializers import ModelSerializer
from rest_framework.serializers import ModelSerializer, Serializer
from rest_framework.viewsets import ModelViewSet
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):
"""NotificationTransport Viewset"""
queryset = NotificationTransport.objects.all()
serializer_class = NotificationTransportSerializer
@swagger_auto_schema(
responses={200: NotificationTransportTestSerializer(many=False)},
request_body=no_body,
)
@action(detail=True, methods=["post"])
# pylint: disable=invalid-name
def test(self, request: Request, pk=None) -> Response:
@ -61,6 +78,10 @@ class NotificationTransportViewSet(ModelViewSet):
user=request.user,
)
try:
return Response(transport.send(notification))
response = NotificationTransportTestSerializer(
data={"messages": transport.send(notification)}
)
response.is_valid()
return Response(response.data)
except NotificationTransportError as exc:
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
is currently active"""
type = ChoiceField(choices=list(ChallengeTypes))
type = ChoiceField(
choices=[(x.name, x.name) for x in ChallengeTypes],
)
component = CharField(required=False)
title = CharField(required=False)
@ -90,7 +92,7 @@ class ChallengeResponse(Serializer):
stage: Optional["StageView"]
def __init__(self, instance, data, **kwargs):
def __init__(self, instance=None, data=None, **kwargs):
self.stage = kwargs.pop("stage", None)
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.response import HttpResponse
from django.views.generic.base import View
from rest_framework.request import Request
from structlog.stdlib import get_logger
from authentik.core.models import DEFAULT_AVATAR, User
@ -67,9 +68,9 @@ class ChallengeStageView(StageView):
return HttpChallengeResponse(challenge)
# pylint: disable=unused-argument
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
def post(self, request: Request, *args, **kwargs) -> HttpResponse:
"""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():
return self.challenge_invalid(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.views.decorators.clickjacking import xframe_options_sameorigin
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 authentik.core.models import USER_ATTRIBUTE_DEBUG
from authentik.events.models import cleanse_dict
from authentik.flows.challenge import (
Challenge,
ChallengeResponse,
ChallengeTypes,
HttpChallengeResponse,
RedirectChallenge,
@ -40,9 +45,11 @@ SESSION_KEY_GET = "authentik_flows_get"
@method_decorator(xframe_options_sameorigin, name="dispatch")
class FlowExecutorView(View):
class FlowExecutorView(APIView):
"""Stage 1 Flow executor, passing requests to Stage Views"""
permission_classes = [AllowAny]
flow: Flow
plan: Optional[FlowPlan] = None
@ -113,8 +120,13 @@ class FlowExecutorView(View):
self.current_stage_view.request = 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:
"""pass get request to current stage"""
"""Get the next pending challenge from the currently active flow."""
self._logger.debug(
"f(exec): Passing GET",
view_class=class_to_path(self.current_stage_view.__class__),
@ -127,8 +139,13 @@ class FlowExecutorView(View):
self._logger.exception(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:
"""pass post request to current stage"""
"""Solve the previously retrieved challenge and advanced to the next stage."""
self._logger.debug(
"f(exec): Passing POST",
view_class=class_to_path(self.current_stage_view.__class__),
@ -175,8 +192,10 @@ class FlowExecutorView(View):
"f(exec): Continuing with next stage",
reamining=len(self.plan.stages),
)
kwargs = self.kwargs
kwargs.update({"flow_slug": self.flow.slug})
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
self._logger.debug(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -279,6 +279,7 @@ stages:
displayName: Build static files for e2e
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
cd web
npm i
npm run build

View File

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

View File

@ -29,10 +29,7 @@ paths:
'200':
description: Login Metrics per 1h
schema:
description: ''
type: array
items:
$ref: '#/definitions/AdministrationMetrics'
$ref: '#/definitions/LoginMetrics'
tags:
- admin
parameters: []
@ -318,9 +315,12 @@ paths:
parameters: []
responses:
'200':
description: ''
description: Coordinates for diagrams
schema:
$ref: '#/definitions/Application'
description: ''
type: array
items:
$ref: '#/definitions/Coordinate'
tags:
- core
parameters:
@ -1620,17 +1620,12 @@ paths:
description: |-
Send example notification using selected transport. Requires
Modify permissions.
parameters:
- name: data
in: body
required: true
schema:
$ref: '#/definitions/NotificationTransport'
parameters: []
responses:
'201':
description: ''
'200':
description: Notification test serializer
schema:
$ref: '#/definitions/NotificationTransport'
$ref: '#/definitions/NotificationTransportTest'
tags:
- events
parameters:
@ -1822,6 +1817,42 @@ paths:
required: true
type: string
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/:
get:
operationId: flows_instances_list
@ -9296,17 +9327,33 @@ paths:
type: string
format: uuid
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
type: object
properties:
logins_per_1h:
title: Logins per 1h
type: string
description: ''
type: array
items:
$ref: '#/definitions/Coordinate'
readOnly: true
logins_failed_per_1h:
title: Logins failed per 1h
type: string
description: ''
type: array
items:
$ref: '#/definitions/Coordinate'
readOnly: true
Task:
description: Serialize TaskInfo and TaskResult
@ -9332,11 +9379,11 @@ definitions:
format: date-time
status:
title: Status
type: integer
type: string
enum:
- 1
- 2
- 4
- SUCCESSFUL
- WARNING
- ERROR
messages:
description: ''
type: array
@ -9728,8 +9775,6 @@ definitions:
type: integer
Notification:
description: Notification Serializer
required:
- event
type: object
properties:
pk:
@ -9897,6 +9942,18 @@ definitions:
webhook_url:
title: Webhook url
type: string
NotificationTransportTest:
description: Notification test serializer
required:
- messages
type: object
properties:
messages:
description: ''
type: array
items:
type: string
minLength: 1
Flow:
description: Flow Serializer
required:
@ -10050,6 +10107,55 @@ definitions:
format: uuid
readOnly: 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:
description: Generic cache stats for an object
type: object
@ -10303,22 +10409,23 @@ definitions:
readOnly: true
TypeCreate:
description: Types of an object that can be created
required:
- name
- description
- link
type: object
properties:
name:
title: Name
type: string
readOnly: true
minLength: 1
description:
title: Description
type: string
readOnly: true
minLength: 1
link:
title: Link
type: string
readOnly: true
minLength: 1
ServiceConnectionState:
description: Serializer for Service connection state

View File

@ -4,3 +4,8 @@ node_modules
dist
# don't lint nyc coverage output
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/', '') }}
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
jobs:
- job: eslint
@ -20,6 +39,11 @@ stages:
inputs:
versionSpec: '12.x'
displayName: 'Install Node.js'
- task: DownloadPipelineArtifact@2
inputs:
buildType: 'current'
artifactName: 'ts_swagger_client'
path: "web/src/api/"
- task: Npm@1
inputs:
command: 'install'
@ -37,6 +61,11 @@ stages:
inputs:
versionSpec: '12.x'
displayName: 'Install Node.js'
- task: DownloadPipelineArtifact@2
inputs:
buildType: 'current'
artifactName: 'ts_swagger_client'
path: "web/src/api/"
- task: Npm@1
inputs:
command: 'install'
@ -56,6 +85,11 @@ stages:
inputs:
versionSpec: '12.x'
displayName: 'Install Node.js'
- task: DownloadPipelineArtifact@2
inputs:
buildType: 'current'
artifactName: 'ts_swagger_client'
path: "web/src/api/"
- task: Npm@1
inputs:
command: 'install'
@ -71,16 +105,21 @@ stages:
pool:
vmImage: 'ubuntu-latest'
steps:
- task: Bash@3
inputs:
targetType: 'inline'
script: |
python ./scripts/az_do_set_branch.py
- task: Docker@2
inputs:
containerRegistry: 'beryjuorg-harbor'
repository: 'authentik/static'
command: 'buildAndPush'
Dockerfile: 'web/Dockerfile'
tags: "gh-$(branchName)"
buildContext: 'web/'
- task: DownloadPipelineArtifact@2
inputs:
buildType: 'current'
artifactName: 'ts_swagger_client'
path: "web/src/api/"
- task: Bash@3
inputs:
targetType: 'inline'
script: |
python ./scripts/az_do_set_branch.py
- task: Docker@2
inputs:
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 {
page?: number;
page_size?: number;
@ -20,97 +13,20 @@ export interface BaseInheritanceModel {
}
export class Client {
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 {
export interface AKPagination {
next?: number;
previous?: number;
count: number;
current: number;
total_pages: number;
totalPages: number;
start_index: number;
end_index: number;
startIndex: number;
endIndex: number;
}
export interface AKResponse<T> {
pagination: PBPagination;
pagination: AKPagination;
results: Array<T>;
}

View File

@ -1,42 +1,39 @@
import { DefaultClient } from "./Client";
import * as Sentry from "@sentry/browser";
import { Integrations } from "@sentry/tracing";
import { VERSION } from "../constants";
import { SentryIgnoredError } from "../common/errors";
import { Configuration } from "./runtime";
import { RootApi } from "./apis";
import { Config } from ".";
import { getCookie } from "../utils";
export class Config {
branding_logo: string;
branding_title: string;
error_reporting_enabled: boolean;
error_reporting_environment: string;
error_reporting_send_pii: boolean;
constructor() {
throw Error();
export const DEFAULT_CONFIG = new Configuration({
basePath: "/api/v2beta",
headers: {
"X-CSRFToken": getCookie("authentik_csrf"),
}
});
static get(): Promise<Config> {
return DefaultClient.fetch<Config>(["root", "config"]).then((config) => {
if (config.error_reporting_enabled) {
Sentry.init({
dsn: "https://a579bb09306d4f8b8d8847c052d3a1d3@sentry.beryju.org/8",
release: `authentik@${VERSION}`,
integrations: [
new Integrations.BrowserTracing(),
],
tracesSampleRate: 0.6,
environment: config.error_reporting_environment,
beforeSend(event: Sentry.Event, hint: Sentry.EventHint) {
if (hint.originalException instanceof SentryIgnoredError) {
return null;
}
return event;
},
});
console.debug("authentik/config: Sentry enabled.");
}
return config;
});
}
export function configureSentry(): Promise<Config> {
return new RootApi(DEFAULT_CONFIG).rootConfigList().then((config) => {
if (config.errorReportingEnabled) {
Sentry.init({
dsn: "https://a579bb09306d4f8b8d8847c052d3a1d3@sentry.beryju.org/8",
release: `authentik@${VERSION}`,
integrations: [
new Integrations.BrowserTracing(),
],
tracesSampleRate: 0.6,
environment: config.errorReportingEnvironment,
beforeSend(event: Sentry.Event, hint: Sentry.EventHint) {
if (hint.originalException instanceof SentryIgnoredError) {
return null;
}
return event;
},
});
console.debug("authentik/config: Sentry enabled.");
}
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 {
pk: number;
@ -11,37 +11,7 @@ export interface EventContext {
[key: string]: EventContext | string | number | string[];
}
export class Event {
pk: string;
export interface EventWithContext extends Event {
user: EventUser;
action: string;
app: string;
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 { TypeCreate } from "./Providers";
export enum ChallengeTypes {
native = "native",
response = "response",
shell = "shell",
redirect = "redirect",
}
import { ChallengeTypeEnum } from "./models";
export interface Error {
code: string;
@ -18,11 +10,12 @@ export interface ErrorDict {
}
export interface Challenge {
type: ChallengeTypes;
type: ChallengeTypeEnum;
component?: string;
title?: string;
response_errors?: ErrorDict;
}
export interface WithUserInfoChallenge extends Challenge {
pending_user: string;
pending_user_avatar: string;
@ -31,6 +24,7 @@ export interface WithUserInfoChallenge extends Challenge {
export interface ShellChallenge extends Challenge {
body: string;
}
export interface RedirectChallenge extends Challenge {
to: string;
}
@ -44,104 +38,3 @@ export enum FlowDesignation {
Recovery = "recovery",
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>;
export class User {
pk: number;
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;
});
export function me(): Promise<User> {
if (!_globalMePromise) {
_globalMePromise = new CoreApi(DEFAULT_CONFIG).coreUsersMe({});
}
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;
}
/* tabs, vertical */
.pf-c-tabs__link {
.pf-c-tabs.pf-m-vertical .pf-c-tabs__link {
background-color: var(--ak-dark-background-light);
}
/* 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 { ERROR_CLASS, SUCCESS_CLASS } from "../../constants";
import { SpinnerButton } from "./SpinnerButton";
@ -12,43 +11,33 @@ export class ActionButton extends SpinnerButton {
@property()
method = "POST";
@property({attribute: false})
// eslint-disable-next-line @typescript-eslint/no-explicit-any
apiRequest: () => Promise<any> = () => { throw new Error(); };
callAction(): void {
if (this.isRunning === true) {
return;
}
this.setLoading();
const csrftoken = getCookie("authentik_csrf");
const request = new Request(this.url, {
headers: { "X-CSRFToken": csrftoken },
});
fetch(request, {
method: this.method,
mode: "same-origin",
this.apiRequest().then(() => {
this.setDone(SUCCESS_CLASS);
})
.then((r) => {
if (!r.ok) {
throw r;
}
return r;
})
.then(() => {
this.setDone(SUCCESS_CLASS);
})
.catch((e: Error | Response) => {
if (e instanceof Error) {
.catch((e: Error | Response) => {
if (e instanceof Error) {
showMessage({
level_tag: "error",
message: e.toString()
});
} else {
e.text().then(t => {
showMessage({
level_tag: "error",
message: e.toString()
message: t
});
} else {
e.text().then(t => {
showMessage({
level_tag: "error",
message: t
});
});
}
this.setDone(ERROR_CLASS);
});
});
}
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";
// @ts-ignore
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 { ColorStyles } from "../../common/styles";
import { DEFAULT_CONFIG } from "../../api/Config";
@customElement("ak-token-copy-button")
export class TokenCopyButton extends LitElement {
@ -36,8 +37,14 @@ export class TokenCopyButton extends LitElement {
}, 1500);
return;
}
Token.getKey(this.identifier).then((token) => {
navigator.clipboard.writeText(token).then(() => {
new CoreApi(DEFAULT_CONFIG).coreTokensViewKey({
identifier: this.identifier
}).then((token) => {
if (!token.key) {
this.buttonClass = ERROR_CLASS;
return;
}
navigator.clipboard.writeText(token.key).then(() => {
this.buttonClass = SUCCESS_CLASS;
setTimeout(() => {
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 {
if (navigator.webdriver) return;
const wsUrl = `${window.location.protocol.replace("http", "ws")}//${
window.location.host
}/ws/client/`;

View File

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

View File

@ -2,16 +2,16 @@ import { gettext } from "django";
import { customElement, html, property, TemplateResult } from "lit-element";
import { AKResponse } from "../../api/Client";
import { Table, TableColumn } from "../../elements/table/Table";
import { PolicyBinding } from "../../api/PolicyBindings";
import { PoliciesApi, PolicyBinding } from "../../api";
import "../../elements/Tabs";
import "../../elements/AdminLoginsChart";
import "../../elements/buttons/ModalButton";
import "../../elements/buttons/SpinnerButton";
import "../../elements/buttons/Dropdown";
import { Policy } from "../../api/Policies";
import { until } from "lit-html/directives/until";
import { PAGE_SIZE } from "../../constants";
import { DEFAULT_CONFIG } from "../../api/Config";
import { AdminURLManager } from "../../api/legacy";
@customElement("ak-bound-policies-list")
export class BoundPoliciesList extends Table<PolicyBinding> {
@ -19,11 +19,11 @@ export class BoundPoliciesList extends Table<PolicyBinding> {
target?: string;
apiEndpoint(page: number): Promise<AKResponse<PolicyBinding>> {
return PolicyBinding.list({
return new PoliciesApi(DEFAULT_CONFIG).policiesBindingsList({
target: this.target || "",
ordering: "order",
page: page,
page_size: PAGE_SIZE,
pageSize: PAGE_SIZE,
});
}
@ -56,13 +56,13 @@ export class BoundPoliciesList extends Table<PolicyBinding> {
html`${item.order}`,
html`${item.timeout}`,
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">
${gettext("Edit")}
</ak-spinner-button>
<div slot="modal"></div>
</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">
${gettext("Delete")}
</ak-spinner-button>
@ -78,7 +78,7 @@ export class BoundPoliciesList extends Table<PolicyBinding> {
${gettext("No policies are currently bound to this object.")}
</div>
<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">
${gettext("Bind Policy")}
</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>
</button>
<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 html`<li>
<ak-modal-button href="${type.link}">
@ -110,7 +110,7 @@ export class BoundPoliciesList extends Table<PolicyBinding> {
}), html`<ak-spinner></ak-spinner>`)}
</ul>
</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">
${gettext("Bind Policy")}
</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";
// @ts-ignore
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 = {
branding_logo: " /static/dist/assets/icons/icon_left_brand.svg",
branding_title: "authentik",
brandingLogo: " /static/dist/assets/icons/icon_left_brand.svg",
brandingTitle: "authentik",
error_reporting_enabled: false,
error_reporting_environment: "",
error_reporting_send_pii: false,
errorReportingEnabled: false,
errorReportingEnvironment: "",
errorReportingSendPii: false,
};
@customElement("ak-sidebar-brand")
@ -40,13 +42,13 @@ export class SidebarBrand extends LitElement {
}
firstUpdated(): void {
Config.get().then((c) => (this.config = c));
configureSentry().then((c) => {this.config = c;});
}
render(): TemplateResult {
return html` <a href="#/" class="pf-c-page__header-brand-link">
<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>
</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";
// @ts-ignore
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 "../notifications/NotificationTrigger";
import { ifDefined } from "lit-html/directives/if-defined";
@customElement("ak-sidebar-user")
export class SidebarUser extends LitElement {
@ -37,8 +38,8 @@ export class SidebarUser extends LitElement {
render(): TemplateResult {
return html`
<a href="#/-/user/" class="pf-c-nav__link user-avatar" id="user-settings">
${until(User.me().then((u) => {
return html`<img class="pf-c-avatar" src="${u.avatar}" alt="" />`;
${until(me().then((u) => {
return html`<img class="pf-c-avatar" src="${ifDefined(u.avatar)}" alt="" />`;
}), html``)}
</a>
<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 "../EmptyState";
import "../Spinner";
export class TableColumn {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import { gettext } from "django";
import { CSSResult, customElement, html, property, TemplateResult } from "lit-element";
import { COMMON_STYLES } from "../../../common/styles";
import { SpinnerSize } from "../../Spinner";
import { SpinnerSize } from "../../../elements/Spinner";
import { transformAssertionForServer, transformCredentialRequestOptions } from "../authenticator_webauthn/utils";
import { BaseStage } from "../base";
import { AuthenticatorValidateStage, AuthenticatorValidateStageChallenge, DeviceChallenge } from "./AuthenticatorValidateStage";

View File

@ -1,7 +1,7 @@
import { gettext } from "django";
import { customElement, html, property, TemplateResult } from "lit-element";
import { WithUserInfoChallenge } from "../../../api/Flows";
import { SpinnerSize } from "../../Spinner";
import { SpinnerSize } from "../../../elements/Spinner";
import { BaseStage } from "../base";
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 { COMMON_STYLES } from "../../../common/styles";
import { BaseStage } from "../base";
import "../../Spinner";
import "../../../elements/Spinner";
import "../../../elements/utils/LoadingState";
export interface AutosubmitChallenge extends WithUserInfoChallenge {
url: string;

View File

@ -1,7 +1,7 @@
import { LitElement } from "lit-element";
export interface StageHost {
submit(formData?: FormData): Promise<void>;
submit<T>(formData?: T): Promise<void>;
}
export class BaseStage extends LitElement {
@ -10,8 +10,12 @@ export class BaseStage extends LitElement {
submitForm(e: Event): void {
e.preventDefault();
const object: {
[key: string]: unknown;
} = {};
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 { WithUserInfoChallenge } from "../../../api/Flows";
import { COMMON_STYLES } from "../../../common/styles";
import { SpinnerSize } from "../../Spinner";
import { SpinnerSize } from "../../../elements/Spinner";
import { BaseStage } from "../base";
import "../form";
import "../../../elements/utils/LoadingState";
export interface CaptchaChallenge extends WithUserInfoChallenge {
site_key: string;

View File

@ -3,6 +3,7 @@ import { CSSResult, customElement, html, property, TemplateResult } from "lit-el
import { WithUserInfoChallenge } from "../../../api/Flows";
import { COMMON_STYLES } from "../../../common/styles";
import { BaseStage } from "../base";
import "../../../elements/utils/LoadingState";
export interface Permission {
name: string;

View File

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

View File

@ -1,9 +1,10 @@
import { gettext } from "django";
import { CSSResult, customElement, html, property, TemplateResult } from "lit-element";
import { Challenge } from "../../../api/Flows";
import { COMMON_STYLES } from "../../../common/styles";
import { BaseStage } from "../base";
import "../form";
import "../../../elements/utils/LoadingState";
import { Challenge } from "../../../api/Flows";
export interface IdentificationChallenge extends Challenge {

View File

@ -4,6 +4,7 @@ import { WithUserInfoChallenge } from "../../../api/Flows";
import { COMMON_STYLES } from "../../../common/styles";
import { BaseStage } from "../base";
import "../form";
import "../../../elements/utils/LoadingState";
export interface PasswordChallenge extends WithUserInfoChallenge {
recovery_url?: string;

View File

@ -1,10 +1,11 @@
import { gettext } from "django";
import { CSSResult, customElement, html, property, TemplateResult } from "lit-element";
import { unsafeHTML } from "lit-html/directives/unsafe-html";
import { Challenge } from "../../../api/Flows";
import { COMMON_STYLES } from "../../../common/styles";
import { BaseStage } from "../base";
import "../form";
import "../../../elements/utils/LoadingState";
import { Challenge } from "../../../api/Flows";
export interface Prompt {
field_key: string;

View File

@ -1,5 +1,5 @@
import { customElement } from "lit-element";
import { User } from "../api/Users";
import { me } from "../api/Users";
import { SidebarItem } from "../elements/sidebar/Sidebar";
import { ID_REGEX, SLUG_REGEX, UUID_REGEX } from "../elements/router/Route";
import { Interface } from "./Interface";
@ -10,7 +10,7 @@ export const SIDEBAR_ITEMS: SidebarItem[] = [
new SidebarItem("Overview", "/administration/overview"),
new SidebarItem("System Tasks", "/administration/system-tasks"),
).when((): Promise<boolean> => {
return User.me().then(u => u.is_superuser);
return me().then(u => u.isSuperuser||false);
}),
new SidebarItem("Events").children(
new SidebarItem("Log", "/events/log").activeWhen(
@ -19,7 +19,7 @@ export const SIDEBAR_ITEMS: SidebarItem[] = [
new SidebarItem("Notification Rules", "/events/rules"),
new SidebarItem("Notification Transports", "/events/transports"),
).when((): Promise<boolean> => {
return User.me().then(u => u.is_superuser);
return me().then(u => u.isSuperuser||false);
}),
new SidebarItem("Resources").children(
new SidebarItem("Applications", "/core/applications").activeWhen(
@ -34,13 +34,13 @@ export const SIDEBAR_ITEMS: SidebarItem[] = [
new SidebarItem("Outposts", "/outpost/outposts"),
new SidebarItem("Outpost Service Connections", "/outpost/service-connections"),
).when((): Promise<boolean> => {
return User.me().then(u => u.is_superuser);
return me().then(u => u.isSuperuser||false);
}),
new SidebarItem("Customisation").children(
new SidebarItem("Policies", "/policy/policies"),
new SidebarItem("Property Mappings", "/core/property-mappings"),
).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", "/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("Invitations", "/flow/stages/invitations"),
).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("User", "/identity/users"),
@ -56,7 +56,7 @@ export const SIDEBAR_ITEMS: SidebarItem[] = [
new SidebarItem("Certificates", "/crypto/certificates"),
new SidebarItem("Tokens", "/core/tokens"),
).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";
// Elements that are used by SiteShell pages
// And can't dynamically be imported
import "./elements/buttons/ActionButton";
import "./elements/buttons/Dropdown";
import "./elements/buttons/ModalButton";
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/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/LibraryPage";
import "./pages/generic/SiteShell";
import "./interfaces/AdminInterface";

View File

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

View File

@ -2,7 +2,7 @@ import { gettext } from "django";
import { CSSResult, customElement, html, LitElement, TemplateResult } from "lit-element";
import { COMMON_STYLES } from "../../common/styles";
import "../../elements/AdminLoginsChart";
import "../../elements/charts/AdminLoginsChart";
import "../../elements/cards/AggregatePromiseCard";
import "./TopApplicationsTable";
import "./cards/AdminStatusCard";
@ -30,7 +30,7 @@ export class AdminOverviewPage extends LitElement {
<section class="pf-c-page__main-section">
<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-admin-logins-chart .url="${["admin", "metrics"]}"></ak-admin-logins-chart>
<ak-charts-admin-login></ak-charts-admin-login>
</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-top-applications-table></ak-top-applications-table>

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