sources/oauth: migrate to web

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
Jens Langhammer 2021-04-02 15:15:19 +02:00
parent 22f50aae45
commit 7fad2b6563
15 changed files with 387 additions and 100 deletions

View file

@ -1,16 +1,9 @@
"""authentik URL Configuration"""
from django.urls import path
from authentik.admin.views import policies, sources, stages
from authentik.admin.views import policies, stages
urlpatterns = [
# Sources
path("sources/create/", sources.SourceCreateView.as_view(), name="source-create"),
path(
"sources/<uuid:pk>/update/",
sources.SourceUpdateView.as_view(),
name="source-update",
),
# Policies
path("policies/create/", policies.PolicyCreateView.as_view(), name="policy-create"),
path(

View file

@ -1,43 +0,0 @@
"""authentik Source administration"""
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.mixins import (
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
)
from django.contrib.messages.views import SuccessMessageMixin
from django.utils.translation import gettext as _
from guardian.mixins import PermissionRequiredMixin
from authentik.admin.views.utils import InheritanceCreateView, InheritanceUpdateView
from authentik.core.models import Source
class SourceCreateView(
SuccessMessageMixin,
LoginRequiredMixin,
DjangoPermissionRequiredMixin,
InheritanceCreateView,
):
"""Create new Source"""
model = Source
permission_required = "authentik_core.add_source"
success_url = "/"
template_name = "generic/create.html"
success_message = _("Successfully created Source")
class SourceUpdateView(
SuccessMessageMixin,
LoginRequiredMixin,
PermissionRequiredMixin,
InheritanceUpdateView,
):
"""Update source"""
model = Source
permission_required = "authentik_core.change_source"
success_url = "/"
template_name = "generic/update.html"
success_message = _("Successfully updated Source")

View file

@ -10,7 +10,6 @@ from django.contrib.auth.models import AbstractUser
from django.contrib.auth.models import UserManager as DjangoUserManager
from django.db import models
from django.db.models import Q, QuerySet
from django.forms import ModelForm
from django.http import HttpRequest
from django.templatetags.static import static
from django.utils.functional import cached_property
@ -280,11 +279,6 @@ class Source(SerializerModel, PolicyBindingModel):
"""Return component used to edit this object"""
raise NotImplementedError
@property
def form(self) -> Type[ModelForm]:
"""Return Form class used to edit this object"""
raise NotImplementedError
@property
def ui_login_button(self) -> Optional[UILoginButton]:
"""If source uses a http-based flow, return UI Information about the login

View file

@ -1,12 +1,11 @@
"""OAuth Source Serializer"""
from django.urls.base import reverse_lazy
from drf_yasg.utils import swagger_auto_schema
from drf_yasg.utils import swagger_auto_schema, swagger_serializer_method
from rest_framework.decorators import action
from rest_framework.fields import BooleanField, CharField, SerializerMethodField
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
from drf_yasg.utils import swagger_serializer_method
from authentik.core.api.sources import SourceSerializer
from authentik.core.api.utils import PassiveSerializer
@ -61,7 +60,6 @@ class OAuthSourceSerializer(SourceSerializer):
"callback_url",
"type",
]
extra_kwargs = {"consumer_secret": {"write_only": True}}
class OAuthSourceViewSet(ModelViewSet):

View file

@ -47,4 +47,11 @@ class Migration(migrations.Migration):
verbose_name="Profile URL",
),
),
migrations.AlterModelOptions(
name="oauthsource",
options={
"verbose_name": "OAuth Source",
"verbose_name_plural": "OAuth Sources",
},
),
]

View file

@ -86,8 +86,8 @@ class OAuthSource(Source):
class Meta:
verbose_name = _("Generic OAuth Source")
verbose_name_plural = _("Generic OAuth Sources")
verbose_name = _("OAuth Source")
verbose_name_plural = _("OAuth Sources")
class GitHubOAuthSource(OAuthSource):

View file

@ -14561,9 +14561,12 @@ definitions:
format: uuid
readOnly: true
managed:
title: Managed
title: Managed by authentik
description: Objects which are managed by authentik. These objects are created
and updated automatically. This is flag only indicates that an object can
be overwritten by migrations. You can still modify the objects via the API,
but expect changes to be overwritten in a later update.
type: string
readOnly: true
minLength: 1
x-nullable: true
identifier:
@ -16225,9 +16228,12 @@ definitions:
format: uuid
readOnly: true
managed:
title: Managed
title: Managed by authentik
description: Objects which are managed by authentik. These objects are created
and updated automatically. This is flag only indicates that an object can
be overwritten by migrations. You can still modify the objects via the API,
but expect changes to be overwritten in a later update.
type: string
readOnly: true
minLength: 1
x-nullable: true
name:
@ -16275,9 +16281,12 @@ definitions:
format: uuid
readOnly: true
managed:
title: Managed
title: Managed by authentik
description: Objects which are managed by authentik. These objects are created
and updated automatically. This is flag only indicates that an object can
be overwritten by migrations. You can still modify the objects via the API,
but expect changes to be overwritten in a later update.
type: string
readOnly: true
minLength: 1
x-nullable: true
name:
@ -16317,9 +16326,12 @@ definitions:
format: uuid
readOnly: true
managed:
title: Managed
title: Managed by authentik
description: Objects which are managed by authentik. These objects are created
and updated automatically. This is flag only indicates that an object can
be overwritten by migrations. You can still modify the objects via the API,
but expect changes to be overwritten in a later update.
type: string
readOnly: true
minLength: 1
x-nullable: true
name:
@ -16363,9 +16375,12 @@ definitions:
format: uuid
readOnly: true
managed:
title: Managed
title: Managed by authentik
description: Objects which are managed by authentik. These objects are created
and updated automatically. This is flag only indicates that an object can
be overwritten by migrations. You can still modify the objects via the API,
but expect changes to be overwritten in a later update.
type: string
readOnly: true
minLength: 1
x-nullable: true
name:

View file

@ -8,10 +8,6 @@ export class AdminURLManager {
return `/administration/stages/${rest}`;
}
static sources(rest: string): string {
return `/administration/sources/${rest}`;
}
}
export class AppURLManager {

86
web/src/pages/TestPage.ts Normal file
View file

@ -0,0 +1,86 @@
import { CSSResult, customElement, html, LitElement, TemplateResult } from "lit-element";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
import AKGlobal from "../authentik.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import "../elements/forms/FormGroup";
@customElement("ak-test-page")
export class TestPage extends LitElement {
static get styles(): CSSResult[] {
return [PFBase, PFForm, PFButton, PFFormControl, AKGlobal];
}
render(): TemplateResult {
return html`<form novalidate class="pf-c-form">
<div class="pf-c-form__group">
<div class="pf-c-form__group-label">
<label class="pf-c-form__label" for="form-expandable-field-groups-label1">
<span class="pf-c-form__label-text">Label 1</span>
<span class="pf-c-form__label-required" aria-hidden="true">&#42;</span>
</label>
<button class="pf-c-form__group-label-help" aria-label="More info">
<i class="pficon pf-icon-help" aria-hidden="true"></i>
</button>
</div>
<div class="pf-c-form__group-control">
<input class="pf-c-form-control" type="text" id="form-expandable-field-groups-label1"
name="form-expandable-field-groups-label1" required />
</div>
</div>
<div class="pf-c-form__group">
<div class="pf-c-form__group-label">
<label class="pf-c-form__label" for="form-expandable-field-groups-label2">
<span class="pf-c-form__label-text">Label 2</span>
<span class="pf-c-form__label-required" aria-hidden="true">&#42;</span>
</label>
<button class="pf-c-form__group-label-help" aria-label="More info">
<i class="pficon pf-icon-help" aria-hidden="true"></i>
</button>
</div>
<div class="pf-c-form__group-control">
<input class="pf-c-form-control" type="text" id="form-expandable-field-groups-label2"
name="form-expandable-field-groups-label2" required />
</div>
</div>
<ak-form-group>
<span slot="header">
foo
</span>
<div slot="body">
<div class="pf-c-form__group">
<div class="pf-c-form__group-label">
<label class="pf-c-form__label" for="form-expandable-field-groups-field-group2-label1">
<span class="pf-c-form__label-text">Label 1</span>
<span class="pf-c-form__label-required" aria-hidden="true">*</span>
</label>
<button class="pf-c-form__group-label-help" aria-label="More info">
<i class="pficon pf-icon-help" aria-hidden="true"></i>
</button>
</div>
<div class="pf-c-form__group-control">
<input class="pf-c-form-control" type="text" id="form-expandable-field-groups-field-group2-label1" name="form-expandable-field-groups-field-group2-label1" required="">
</div>
</div>
<div class="pf-c-form__group">
<div class="pf-c-form__group-label">
<label class="pf-c-form__label" for="form-expandable-field-groups-field-group2-label2">
<span class="pf-c-form__label-text">Label 2</span>
<span class="pf-c-form__label-required" aria-hidden="true">*</span>
</label>
<button class="pf-c-form__group-label-help" aria-label="More info">
<i class="pficon pf-icon-help" aria-hidden="true"></i>
</button>
</div>
<div class="pf-c-form__group-control">
<input class="pf-c-form-control" type="text" id="form-expandable-field-groups-field-group2-label2" name="form-expandable-field-groups-field-group2-label2" required="">
</div>
</div>
</div>
</ak-form-group>
</form>`;
}
}

View file

@ -16,6 +16,7 @@ import { DEFAULT_CONFIG } from "../../api/Config";
import { ifDefined } from "lit-html/directives/if-defined";
import "./ldap/LDAPSourceForm";
import "./saml/SAMLSourceForm";
import "./oauth/OAuthSourceForm";
@customElement("ak-source-list")
export class SourceListPage extends TablePage<Source> {
@ -76,6 +77,7 @@ export class SourceListPage extends TablePage<Source> {
.typeMap=${{
"ldap": "ak-source-ldap-form",
"saml": "ak-source-saml-form",
"oauth": "ak-source-oauth-form",
}}>
</ak-proxy-form>
<button slot="trigger" class="pf-c-button pf-m-secondary">

View file

@ -11,18 +11,19 @@ import PFFlex from "@patternfly/patternfly/utilities/Flex/flex.css";
import PFDisplay from "@patternfly/patternfly/utilities/Display/display.css";
import AKGlobal from "../../../authentik.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import "../../../elements/buttons/ModalButton";
import "../../../elements/buttons/SpinnerButton";
import "../../../elements/buttons/ActionButton";
import "../../../elements/CodeMirror";
import "../../../elements/Tabs";
import "../../../elements/events/ObjectChangelog";
import "../../../elements/forms/ModalForm";
import "./LDAPSourceForm";
import { Page } from "../../../elements/Page";
import { until } from "lit-html/directives/until";
import { LDAPSource, SourcesApi } from "authentik-api";
import { DEFAULT_CONFIG } from "../../../api/Config";
import { AdminURLManager } from "../../../api/legacy";
import { EVENT_REFRESH } from "../../../constants";
@customElement("ak-source-ldap-view")
@ -50,7 +51,7 @@ export class LDAPSourceViewPage extends Page {
source!: LDAPSource;
static get styles(): CSSResult[] {
return [PFBase, PFPage, PFFlex, PFDisplay, PFGallery, PFContent, PFCard, PFDescriptionList, PFSizing, AKGlobal];
return [PFBase, PFPage, PFButton, PFFlex, PFDisplay, PFGallery, PFContent, PFCard, PFDescriptionList, PFSizing, AKGlobal];
}
constructor() {
@ -103,12 +104,21 @@ export class LDAPSourceViewPage extends Page {
</dl>
</div>
<div class="pf-c-card__footer">
<ak-modal-button href="${AdminURLManager.sources(`${this.source.pk}/update/`)}">
<ak-spinner-button slot="trigger" class="pf-m-primary">
<ak-forms-modal>
<span slot="submit">
${gettext("Update")}
</span>
<span slot="header">
${gettext("Update LDAP Source")}
</span>
<ak-source-ldap-form
slot="form"
.sourceSlug=${this.source.slug}>
</ak-source-ldap-form>
<button slot="trigger" class="pf-c-button pf-m-primary">
${gettext("Edit")}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
</button>
</ak-forms-modal>
</div>
</div>
</div>

View file

@ -0,0 +1,205 @@
import { OAuthSource, SourcesApi, FlowsApi, FlowDesignationEnum } from "authentik-api";
import { gettext } from "django";
import { customElement, property } from "lit-element";
import { html, TemplateResult } from "lit-html";
import { DEFAULT_CONFIG } from "../../../api/Config";
import { Form } from "../../../elements/forms/Form";
import "../../../elements/forms/FormGroup";
import "../../../elements/forms/HorizontalFormElement";
import { ifDefined } from "lit-html/directives/if-defined";
import { until } from "lit-html/directives/until";
@customElement("ak-source-oauth-form")
export class OAuthSourceForm extends Form<OAuthSource> {
set sourceSlug(value: string) {
new SourcesApi(DEFAULT_CONFIG).sourcesOauthRead({
slug: value,
}).then(source => {
this.source = source;
this.showUrlOptions = source.type?.urlsCustomizable || false;
});
}
@property({attribute: false})
source?: OAuthSource;
@property({type: Boolean})
showUrlOptions = false;
getSuccessMessage(): string {
if (this.source) {
return gettext("Successfully updated source.");
} else {
return gettext("Successfully created source.");
}
}
send = (data: OAuthSource): Promise<OAuthSource> => {
if (this.source) {
return new SourcesApi(DEFAULT_CONFIG).sourcesOauthUpdate({
slug: this.source.slug,
data: data
});
} else {
return new SourcesApi(DEFAULT_CONFIG).sourcesOauthCreate({
data: data
});
}
};
renderUrlOptions(): TemplateResult {
if (!this.showUrlOptions) {
return html``;
}
return html`
<ak-form-group>
<span slot="header">
${gettext("URL settings")}
</span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
label=${gettext("Authorization URL")}
?required=${true}
name="authorizationUrl">
<input type="text" value="${ifDefined(this.source?.authorizationUrl)}" class="pf-c-form-control" required>
<p class="pf-c-form__helper-text">${gettext("URL the user is redirect to to consent the authorization.")}</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${gettext("Access token URL")}
?required=${true}
name="accessTokenUrl">
<input type="text" value="${ifDefined(this.source?.accessTokenUrl)}" class="pf-c-form-control" required>
<p class="pf-c-form__helper-text">${gettext("URL used by authentik to retrieve tokens.")}</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${gettext("Profile URL")}
?required=${true}
name="profileUrl">
<input type="text" value="${ifDefined(this.source?.profileUrl)}" class="pf-c-form-control" required>
<p class="pf-c-form__helper-text">${gettext("URL used by authentik to get user information.")}</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${gettext("Request token URL")}
name="requestTokenUrl">
<input type="text" value="${ifDefined(this.source?.requestTokenUrl)}" class="pf-c-form-control">
<p class="pf-c-form__helper-text">${gettext("URL used to request the initial token. This URL is only required for OAuth 1.")}</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>`;
}
renderForm(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal">
<ak-form-element-horizontal
label=${gettext("Name")}
?required=${true}
name="name">
<input type="text" value="${ifDefined(this.source?.name)}" class="pf-c-form-control" required>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${gettext("Slug")}
?required=${true}
name="slug">
<input type="text" value="${ifDefined(this.source?.slug)}" class="pf-c-form-control" required>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="enabled">
<div class="pf-c-check">
<input type="checkbox" class="pf-c-check__input" ?checked=${this.source?.enabled || true}>
<label class="pf-c-check__label">
${gettext("Enabled")}
</label>
</div>
</ak-form-element-horizontal>
<ak-form-group .expanded=${true}>
<span slot="header">
${gettext("Protocol settings")}
</span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
label=${gettext("Consumer key")}
?required=${true}
name="consumerKey">
<input type="text" value="${ifDefined(this.source?.consumerKey)}" class="pf-c-form-control" required>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${gettext("Consumer secret")}
?required=${true}
name="consumerSecret">
<input type="text" value="${ifDefined(this.source?.consumerSecret)}" class="pf-c-form-control" required>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${gettext("Provider type")}
name="providerType">
<select class="pf-c-form-control" @change=${(ev: Event) => {
const el = (ev.target as HTMLSelectElement);
const selected = el.selectedOptions[0];
if ("data-urls-custom" in selected.attributes) {
this.showUrlOptions = true;
} else {
this.showUrlOptions = false;
}
}}>
${until(new SourcesApi(DEFAULT_CONFIG).sourcesOauthSourceTypes().then(types => {
return types.map(type => {
return html`<option ?data-urls-custom=${type.urlsCustomizable} value=${type.slug} ?selected=${this.source?.providerType === type.slug}>${type.name}</option>`;
});
}))}
</select>
<p class="pf-c-form__helper-text">${gettext("Keypair which is used to sign outgoing requests. Leave empty to disable signing.")}</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
${this.renderUrlOptions()}
<ak-form-group>
<span slot="header">
${gettext("Flow settings")}
</span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
label=${gettext("Authentication flow")}
?required=${true}
name="authenticationFlow">
<select class="pf-c-form-control">
${until(new FlowsApi(DEFAULT_CONFIG).flowsInstancesList({
ordering: "pk",
designation: FlowDesignationEnum.Authentication,
}).then(flows => {
return flows.results.map(flow => {
let selected = this.source?.authenticationFlow === flow.pk;
if (!this.source?.authenticationFlow && flow.slug === "default-source-authentication") {
selected = true;
}
return html`<option value=${ifDefined(flow.pk)} ?selected=${selected}>${flow.name} (${flow.slug})</option>`;
});
}))}
</select>
<p class="pf-c-form__helper-text">${gettext("Flow to use when authenticating existing users.")}</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${gettext("Enrollment flow")}
?required=${true}
name="enrollmentFlow">
<select class="pf-c-form-control">
${until(new FlowsApi(DEFAULT_CONFIG).flowsInstancesList({
ordering: "pk",
designation: FlowDesignationEnum.Enrollment,
}).then(flows => {
return flows.results.map(flow => {
let selected = this.source?.enrollmentFlow === flow.pk;
if (!this.source?.enrollmentFlow && flow.slug === "default-source-enrollment") {
selected = true;
}
return html`<option value=${ifDefined(flow.pk)} ?selected=${selected}>${flow.name} (${flow.slug})</option>`;
});
}))}
</select>
<p class="pf-c-form__helper-text">${gettext("Flow to use when enrolling new users.")}</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
</form>`;
}
}

View file

@ -11,16 +11,17 @@ import PFFlex from "@patternfly/patternfly/utilities/Flex/flex.css";
import PFDisplay from "@patternfly/patternfly/utilities/Display/display.css";
import AKGlobal from "../../../authentik.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import "../../../elements/buttons/ModalButton";
import "../../../elements/buttons/SpinnerButton";
import "../../../elements/CodeMirror";
import "../../../elements/Tabs";
import "../../../elements/events/ObjectChangelog";
import "../../../elements/forms/ModalForm";
import "./OAuthSourceForm";
import { Page } from "../../../elements/Page";
import { OAuthSource, SourcesApi } from "authentik-api";
import { DEFAULT_CONFIG } from "../../../api/Config";
import { AdminURLManager } from "../../../api/legacy";
import { EVENT_REFRESH } from "../../../constants";
@customElement("ak-source-oauth-view")
@ -48,7 +49,7 @@ export class OAuthSourceViewPage extends Page {
source?: OAuthSource;
static get styles(): CSSResult[] {
return [PFBase, PFPage, PFFlex, PFDisplay, PFGallery, PFContent, PFCard, PFDescriptionList, PFSizing, AKGlobal];
return [PFBase, PFPage, PFButton, PFFlex, PFDisplay, PFGallery, PFContent, PFCard, PFDescriptionList, PFSizing, AKGlobal];
}
constructor() {
@ -121,12 +122,21 @@ export class OAuthSourceViewPage extends Page {
</dl>
</div>
<div class="pf-c-card__footer">
<ak-modal-button href="${AdminURLManager.sources(`${this.source.pk}/update/`)}">
<ak-spinner-button slot="trigger" class="pf-m-primary">
<ak-forms-modal>
<span slot="submit">
${gettext("Update")}
</span>
<span slot="header">
${gettext("Update OAuth Source")}
</span>
<ak-source-oauth-form
slot="form"
.sourceSlug=${this.source.slug}>
</ak-source-oauth-form>
<button slot="trigger" class="pf-c-button pf-m-primary">
${gettext("Edit")}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
</button>
</ak-forms-modal>
</div>
</div>
</div>

View file

@ -12,16 +12,18 @@ import PFFlex from "@patternfly/patternfly/utilities/Flex/flex.css";
import PFDisplay from "@patternfly/patternfly/utilities/Display/display.css";
import AKGlobal from "../../../authentik.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import "../../../elements/buttons/ModalButton";
import "../../../elements/buttons/SpinnerButton";
import "../../../elements/CodeMirror";
import "../../../elements/Tabs";
import "../../../elements/events/ObjectChangelog";
import "../../../elements/forms/ModalForm";
import "./SAMLSourceForm";
import { Page } from "../../../elements/Page";
import { SAMLSource, SourcesApi } from "authentik-api";
import { DEFAULT_CONFIG } from "../../../api/Config";
import { AdminURLManager, AppURLManager } from "../../../api/legacy";
import { AppURLManager } from "../../../api/legacy";
import { EVENT_REFRESH } from "../../../constants";
import { ifDefined } from "lit-html/directives/if-defined";
@ -50,7 +52,7 @@ export class SAMLSourceViewPage extends Page {
source?: SAMLSource;
static get styles(): CSSResult[] {
return [PFBase, PFPage, PFFlex, PFDisplay, PFGallery, PFContent, PFCard, PFDescriptionList, PFSizing, AKGlobal];
return [PFBase, PFPage, PFFlex, PFButton, PFDisplay, PFGallery, PFContent, PFCard, PFDescriptionList, PFSizing, AKGlobal];
}
constructor() {
@ -107,12 +109,21 @@ export class SAMLSourceViewPage extends Page {
</dl>
</div>
<div class="pf-c-card__footer">
<ak-modal-button href="${AdminURLManager.sources(`${this.source.pk}/update/`)}">
<ak-spinner-button slot="trigger" class="pf-m-primary">
<ak-forms-modal>
<span slot="submit">
${gettext("Update")}
</span>
<span slot="header">
${gettext("Update SAML Source")}
</span>
<ak-source-saml-form
slot="form"
.sourceSlug=${this.source.slug}>
</ak-source-saml-form>
<button slot="trigger" class="pf-c-button pf-m-primary">
${gettext("Edit")}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
</button>
</ak-forms-modal>
</div>
</div>
</div>

View file

@ -30,6 +30,8 @@ import "./pages/users/UserListPage";
import "./pages/users/UserViewPage";
import "./pages/user-settings/UserSettingsPage";
import "./pages/TestPage";
export const ROUTES: Route[] = [
// Prevent infinite Shell loops
new Route(new RegExp("^/$")).redirect("/library"),
@ -74,4 +76,5 @@ export const ROUTES: Route[] = [
new Route(new RegExp("^/outpost/service-connections$"), html`<ak-outpost-service-connection-list></ak-outpost-service-connection-list>`),
new Route(new RegExp("^/crypto/certificates$"), html`<ak-crypto-certificate-list></ak-crypto-certificate-list>`),
new Route(new RegExp("^/user$"), html`<ak-user-settings></ak-user-settings>`),
new Route(new RegExp("^/test$"), html`<ak-test-page></ak-test-page>`),
];