Compare commits

...

1 Commits

Author SHA1 Message Date
Teffen Ellis
d0fd4d4a34 web: Flesh out router fixes. 2025-07-25 13:27:59 +02:00
66 changed files with 844 additions and 369 deletions

View File

@@ -2,6 +2,37 @@ import { ID_REGEX, SLUG_REGEX, UUID_REGEX } from "#elements/router/Route";
import { SidebarItemProperties } from "#elements/sidebar/SidebarItem";
import { LitPropertyRecord } from "#elements/types";
import {
toAdministrationDashboardUsers,
toAdministrationOverview,
toAdministrationSystemTasks,
toAdminSettings,
toApplications,
toBlueprintsInstances,
toBrands,
toCryptoCertificates,
toEnterpriseLicenses,
toEventLogs,
toEventsRules,
toEventsTransports,
toFlows,
toFlowStages,
toFlowStagesInvitations,
toFlowStagesPrompts,
toIdentityGroups,
toIdentityInitialPermissions,
toIdentityRoles,
toIdentityUsers,
toOutpostIntegrations,
toOutposts,
toPolicyPolicies,
toPolicyReputation,
toPropertyMappings,
toProviders,
toSources,
toTokens,
} from "#admin/navigation";
import { spread } from "@open-wc/lit-helpers";
import { msg } from "@lit/localize";
@@ -50,51 +81,51 @@ export function renderSidebarItems(entries: readonly SidebarEntry[]) {
// prettier-ignore
export const AdminSidebarEntries: readonly SidebarEntry[] = [
[null, msg("Dashboards"), { "?expanded": true }, [
["/administration/overview", msg("Overview")],
["/administration/dashboard/users", msg("User Statistics")],
["/administration/system-tasks", msg("System Tasks")]]
[toAdministrationOverview() , msg("Overview")],
[toAdministrationDashboardUsers() , msg("User Statistics")],
[toAdministrationSystemTasks() , msg("System Tasks")]]
],
[null, msg("Applications"), null, [
["/core/applications", msg("Applications"), [`^/core/applications/(?<slug>${SLUG_REGEX})$`]],
["/core/providers", msg("Providers"), [`^/core/providers/(?<id>${ID_REGEX})$`]],
["/outpost/outposts", msg("Outposts")]]
[toApplications(), msg("Applications"), [`^/core/applications/(?<slug>${SLUG_REGEX})$`]],
[toProviders(), msg("Providers"), [`^/core/providers/(?<id>${ID_REGEX})$`]],
[toOutposts(), msg("Outposts")]]
],
[null, msg("Events"), null, [
["/events/log", msg("Logs"), [`^/events/log/(?<id>${UUID_REGEX})$`]],
["/events/rules", msg("Notification Rules")],
["/events/transports", msg("Notification Transports")]]
[toEventLogs(), msg("Logs"), [`^/events/log/(?<id>${UUID_REGEX})$`]],
[toEventsRules() , msg("Notification Rules")],
[toEventsTransports() , msg("Notification Transports")]]
],
[null, msg("Customization"), null, [
["/policy/policies", msg("Policies")],
["/core/property-mappings", msg("Property Mappings")],
["/blueprints/instances", msg("Blueprints")],
["/policy/reputation", msg("Reputation scores")]]
[toPolicyPolicies() , msg("Policies")],
[toPropertyMappings() , msg("Property Mappings")],
[toBlueprintsInstances() , msg("Blueprints")],
[toPolicyReputation() , msg("Reputation scores")]]
],
[null, msg("Flows and Stages"), null, [
["/flow/flows", msg("Flows"), [`^/flow/flows/(?<slug>${SLUG_REGEX})$`]],
["/flow/stages", msg("Stages")],
["/flow/stages/prompts", msg("Prompts")]]
[toFlows() , msg("Flows"), [`^/flow/flows/(?<slug>${SLUG_REGEX})$`]],
[toFlowStages() , msg("Stages")],
[toFlowStagesPrompts() , msg("Prompts")]]
],
[null, msg("Directory"), null, [
["/identity/users", msg("Users"), [`^/identity/users/(?<id>${ID_REGEX})$`]],
["/identity/groups", msg("Groups"), [`^/identity/groups/(?<id>${UUID_REGEX})$`]],
["/identity/roles", msg("Roles"), [`^/identity/roles/(?<id>${UUID_REGEX})$`]],
["/identity/initial-permissions", msg("Initial Permissions"), [`^/identity/initial-permissions/(?<id>${ID_REGEX})$`]],
["/core/sources", msg("Federation and Social login"), [`^/core/sources/(?<slug>${SLUG_REGEX})$`]],
["/core/tokens", msg("Tokens and App passwords")],
["/flow/stages/invitations", msg("Invitations")]]
[toIdentityUsers() , msg("Users"), [`^/identity/users/(?<id>${ID_REGEX})$`]],
[toIdentityGroups() , msg("Groups"), [`^/identity/groups/(?<id>${UUID_REGEX})$`]],
[toIdentityRoles() , msg("Roles"), [`^/identity/roles/(?<id>${UUID_REGEX})$`]],
[toIdentityInitialPermissions() , msg("Initial Permissions"), [`^/identity/initial-permissions/(?<id>${ID_REGEX})$`]],
[toSources() , msg("Federation and Social login"), [`^/core/sources/(?<slug>${SLUG_REGEX})$`]],
[toTokens() , msg("Tokens and App passwords")],
[toFlowStagesInvitations() , msg("Invitations")]]
],
[null, msg("System"), null, [
["/core/brands", msg("Brands")],
["/crypto/certificates", msg("Certificates")],
["/outpost/integrations", msg("Outpost Integrations")],
["/admin/settings", msg("Settings")]]
[toBrands() , msg("Brands")],
[toCryptoCertificates() , msg("Certificates")],
[toOutpostIntegrations() , msg("Outpost Integrations")],
[toAdminSettings() , msg("Settings")]]
],
];
// prettier-ignore
export const AdminSidebarEnterpriseEntries: readonly SidebarEntry[] = [
[null, msg("Enterprise"), null, [
["/enterprise/licenses", msg("Licenses"), null]
[toEnterpriseLicenses(), msg("Licenses"), null]
],
]]

View File

@@ -22,7 +22,7 @@ import { WebsocketClient } from "#common/ws";
import { AuthenticatedInterface } from "#elements/AuthenticatedInterface";
import { WithCapabilitiesConfig } from "#elements/mixins/capabilities";
import { getURLParam, updateURLParams } from "#elements/router/RouteMatch";
import { getURLParam, navigate, toUserRoute, updateURLParams } from "#elements/router/navigation";
import { SidebarToggleEventDetail } from "#components/ak-page-header";
@@ -51,10 +51,10 @@ export class AdminInterface extends WithCapabilitiesConfig(AuthenticatedInterfac
//#region Properties
@property({ type: Boolean })
public notificationDrawerOpen = getURLParam("notificationDrawerOpen", false);
public notificationDrawerOpen = !!getURLParam("notificationDrawerOpen");
@property({ type: Boolean })
public apiDrawerOpen = getURLParam("apiDrawerOpen", false);
public apiDrawerOpen = !!getURLParam("apiDrawerOpen");
@property({ type: Object, attribute: false })
public user?: SessionUser;
@@ -176,7 +176,7 @@ export class AdminInterface extends WithCapabilitiesConfig(AuthenticatedInterfac
this.user.user.systemPermissions.includes("access_admin_interface");
if (!canAccessAdmin && this.user.user.pk > 0) {
window.location.assign("/if/user/");
navigate(toUserRoute("/"), { mode: "assign" });
}
});
}

View File

@@ -17,7 +17,8 @@ import { me } from "#common/users";
import { AKElement } from "#elements/Base";
import type { QuickAction } from "#elements/cards/QuickActionsCard";
import { WithLicenseSummary } from "#elements/mixins/license";
import { paramURL } from "#elements/router/RouterOutlet";
import { toApplications, toEventLogs, toIdentityUsers } from "#admin/navigation";
import { SessionUser } from "@goauthentik/api";
import { createReleaseNotesURL } from "@goauthentik/core/version";
@@ -64,10 +65,10 @@ export class AdminOverviewPage extends AdminOverviewBase {
];
quickActions: QuickAction[] = [
[msg("Create a new application"), paramURL("/core/applications", { createWizard: true })],
[msg("Check the logs"), paramURL("/events/log")],
[msg("Create a new application"), toApplications({ createWizard: true })],
[msg("Check the logs"), toEventLogs()],
[msg("Explore integrations"), "https://goauthentik.io/integrations/", true],
[msg("Manage users"), paramURL("/identity/users")],
[msg("Manage users"), toIdentityUsers()],
[
msg("Check the release notes"),
createReleaseNotesURL(import.meta.env.AK_VERSION).href,

View File

@@ -3,6 +3,7 @@ import { DEFAULT_CONFIG } from "#common/api/config";
import { SlottedTemplateResult } from "#elements/types";
import { AdminStatus, AdminStatusCard } from "#admin/admin-overview/cards/AdminStatusCard";
import { toOutposts } from "#admin/navigation";
import { AdminApi, OutpostsApi, SystemInfo } from "@goauthentik/api";
@@ -59,7 +60,7 @@ export class SystemStatusCard extends AdminStatusCard<SystemInfo> {
return Promise.resolve<AdminStatus>({
icon: "fa fa-exclamation-triangle pf-m-warning",
message: html`${msg("Embedded outpost is not configured correctly.")}
<a href="#/outpost/outposts">${msg("Check outposts.")}</a>`,
<a href="${toOutposts()}">${msg("Check outposts.")}</a>`,
});
}
if (!value.httpIsSecure && document.location.protocol === "https:") {

View File

@@ -1,6 +1,7 @@
import { DEFAULT_CONFIG } from "#common/api/config";
import { AdminStatus, AdminStatusCard } from "#admin/admin-overview/cards/AdminStatusCard";
import { toOutposts } from "#admin/navigation";
import { AdminApi, Version } from "@goauthentik/api";
@@ -33,7 +34,7 @@ export class VersionStatusCard extends AdminStatusCard<Version> {
return Promise.resolve<AdminStatus>({
icon: "fa fa-exclamation-triangle pf-m-warning",
message: html`${msg("An outpost is on an incorrect version!")}
<a href="#/outpost/outposts">${msg("Check outposts.")}</a>`,
<a href="${toOutposts()}">${msg("Check outposts.")}</a>`,
});
}
if (value.versionLatestValid) {

View File

@@ -10,10 +10,12 @@ import "./ApplicationWizardHint.js";
import { DEFAULT_CONFIG } from "#common/api/config";
import { WithBrandConfig } from "#elements/mixins/branding";
import { getURLParam } from "#elements/router/RouteMatch";
import { getURLParam } from "#elements/router/navigation";
import { PaginatedResponse, TableColumn } from "#elements/table/Table";
import { TablePage } from "#elements/table/TablePage";
import { toApplication } from "#admin/navigation";
import { Application, CoreApi, PoliciesApi } from "@goauthentik/api";
import MDApplication from "~docs/add-secure-apps/applications/index.md";
@@ -122,7 +124,7 @@ export class ApplicationListPage extends WithBrandConfig(TablePage<Application>)
name=${item.name}
icon=${ifDefined(item.metaIcon || undefined)}
></ak-app-icon>`,
html`<a href="#/core/applications/${item.slug}">
html`<a href="${toApplication(item.slug)}">
<div>${item.name}</div>
${item.metaPublisher ? html`<small>${item.metaPublisher}</small>` : html``}
</a>`,
@@ -155,7 +157,7 @@ export class ApplicationListPage extends WithBrandConfig(TablePage<Application>)
}
renderObjectCreate(): TemplateResult {
return html` <ak-application-wizard .open=${getURLParam("createWizard", false)}>
return html` <ak-application-wizard .open=${!!getURLParam("createWizard")}>
<button
slot="trigger"
class="pf-c-button pf-m-primary"
@@ -164,7 +166,7 @@ export class ApplicationListPage extends WithBrandConfig(TablePage<Application>)
${msg("Create with Provider")}
</button>
</ak-application-wizard>
<ak-forms-modal .open=${getURLParam("createForm", false)}>
<ak-forms-modal .open=${!!getURLParam("createForm")}>
<span slot="submit"> ${msg("Create")} </span>
<span slot="header"> ${msg("Create Application")} </span>
<ak-application-form slot="form"> </ak-application-form>

View File

@@ -16,6 +16,8 @@ import { PFSize } from "#common/enums";
import { AKElement } from "#elements/Base";
import { toProvider } from "#admin/navigation";
import {
Application,
CoreApi,
@@ -167,7 +169,9 @@ export class ApplicationViewPage extends AKElement {
return html`
<li>
<a
href="#/core/providers/${provider.pk}"
href="${toProvider(
provider.pk,
)}"
>
${provider.name}
(${provider.verboseName})

View File

@@ -5,7 +5,7 @@ import "#elements/Label";
import "#elements/buttons/ActionButton/ak-action-button";
import { AKElement } from "#elements/Base";
import { getURLParam } from "#elements/router/RouteMatch";
import { getURLParam } from "#elements/router/navigation";
import { ShowHintController, ShowHintControllerHost } from "#components/ak-hint/ShowHintController";
@@ -107,7 +107,7 @@ export class AkApplicationWizardHint extends AKElement implements ShowHintContro
the same time with our new Application Wizard.
<!-- <a href="(link to docs)">Learn more about the wizard here.</a> -->
</p>
<ak-application-wizard .open=${getURLParam("createWizard", false)}>
<ak-application-wizard .open=${!!getURLParam("createWizard")}>
<button
slot="trigger"
class="pf-c-button pf-m-primary"

View File

@@ -5,6 +5,8 @@ import { $PFBase } from "#common/theme";
import { AKElement } from "#elements/Base";
import { WithLicenseSummary } from "#elements/mixins/license";
import { toEnterpriseLicenses } from "#admin/navigation";
import { msg } from "@lit/localize";
import { html, nothing } from "lit";
import { customElement, property } from "lit/decorators.js";
@@ -21,7 +23,7 @@ export class AkLicenceNotice extends WithLicenseSummary(AKElement) {
? nothing
: html`
<ak-alert class="pf-c-radio__description" inline plain>
<a href="#/enterprise/licenses">${this.notice}</a>
<a href="${toEnterpriseLicenses()}">${this.notice}</a>
</ak-alert>
`;
}

View File

@@ -15,6 +15,7 @@ import { TablePage } from "#elements/table/TablePage";
import { SlottedTemplateResult } from "#elements/types";
import { EventGeo, renderEventUser } from "#admin/events/utils";
import { toEventLog } from "#admin/navigation";
import { Event, EventsApi } from "@goauthentik/api";
@@ -114,7 +115,7 @@ export class EventListPage extends WithLicenseSummary(TablePage<Event>) {
html`<div>${item.clientIp || msg("-")}</div>
<small>${EventGeo(item)}</small>`,
html`<span>${item.brand?.name || msg("-")}</span>`,
html`<a href="#/events/log/${item.pk}">
html`<a href="${toEventLog(item.pk)}">
<pf-tooltip position="top" content=${msg("Show details")}>
<i class="fas fa-share-square"></i>
</pf-tooltip>

View File

@@ -13,6 +13,8 @@ import { severityToLabel } from "#common/labels";
import { PaginatedResponse, TableColumn } from "#elements/table/Table";
import { TablePage } from "#elements/table/TablePage";
import { toIdentityGroup } from "#admin/navigation";
import {
EventsApi,
NotificationRule,
@@ -90,7 +92,7 @@ export class RuleListPage extends TablePage<NotificationRule> {
html`${item.name}`,
html`${severityToLabel(item.severity)}`,
html`${item.destinationGroupObj
? html`<a href="#/identity/groups/${item.destinationGroupObj.pk}"
? html`<a href="${toIdentityGroup(item.destinationGroupObj.pk)}"
>${item.destinationGroupObj.name}</a
>`
: msg("-")}`,

View File

@@ -3,6 +3,8 @@ import { truncate } from "#common/utils";
import { SlottedTemplateResult } from "#elements/types";
import { toIdentityUser } from "#admin/navigation";
import { msg, str } from "@lit/localize";
import { html, nothing, TemplateResult } from "lit";
@@ -27,7 +29,7 @@ export function renderEventUser(
const linkOrSpan = (inner: TemplateResult, evu: EventUser) => {
return html`${evu.pk && !evu.is_anonymous
? html`<a href="#/identity/users/${evu.pk}">${inner}</a>`
? html`<a href="${toIdentityUser(evu.pk)}">${inner}</a>`
: html`<span>${inner}</span>`}`;
};

View File

@@ -6,13 +6,14 @@ import "#elements/forms/DeleteBulkForm";
import "#elements/forms/ModalForm";
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
import { AndNext, DEFAULT_CONFIG } from "#common/api/config";
import { DEFAULT_CONFIG } from "#common/api/config";
import { groupBy } from "#common/utils";
import { PaginatedResponse, TableColumn } from "#elements/table/Table";
import { TablePage } from "#elements/table/TablePage";
import { DesignationToLabel } from "#admin/flows/utils";
import { DesignationToLabel, formatFlowURL } from "#admin/flows/utils";
import { toFlow } from "#admin/navigation";
import { Flow, FlowsApi } from "@goauthentik/api";
@@ -88,7 +89,7 @@ export class FlowListPage extends TablePage<Flow> {
row(item: Flow): TemplateResult[] {
return [
html`<div>
<a href="#/flow/flows/${item.slug}">
<a href="${toFlow(item.slug)}">
<code>${item.slug}</code>
</a>
</div>
@@ -109,10 +110,9 @@ export class FlowListPage extends TablePage<Flow> {
<button
class="pf-c-button pf-m-plain"
@click=${() => {
const finalURL = `${window.location.origin}/if/flow/${item.slug}/${AndNext(
`${window.location.pathname}#${window.location.hash}`,
)}`;
window.open(finalURL, "_blank");
const url = formatFlowURL(item);
window.open(url, "_blank");
}}
>
<pf-tooltip position="top" content=${msg("Execute")}>

View File

@@ -8,12 +8,12 @@ import "#components/events/ObjectChangelog";
import "#elements/Tabs";
import "#elements/buttons/SpinnerButton/ak-spinner-button";
import { AndNext, DEFAULT_CONFIG } from "#common/api/config";
import { DEFAULT_CONFIG } from "#common/api/config";
import { isResponseErrorLike } from "#common/errors/network";
import { AKElement } from "#elements/Base";
import { DesignationToLabel } from "#admin/flows/utils";
import { applyNextParam, DesignationToLabel, formatFlowURL } from "#admin/flows/utils";
import { Flow, FlowsApi, RbacPermissionsAssignedByUsersListModelEnum } from "@goauthentik/api";
@@ -67,6 +67,47 @@ export class FlowViewPage extends AKElement {
}
}
#executeFlow = () => {
return new FlowsApi(DEFAULT_CONFIG)
.flowsInstancesExecuteRetrieve({
slug: this.flow.slug,
})
.then(({ link }) => {
const finalURL = URL.canParse(link)
? new URL(link)
: new URL(link, window.location.origin);
applyNextParam(finalURL);
window.open(finalURL, "_blank");
});
};
#executeFlowInspector = () => {
return new FlowsApi(DEFAULT_CONFIG)
.flowsInstancesExecuteRetrieve({
slug: this.flow.slug,
})
.then(({ link }) => {
const finalURL = URL.canParse(link)
? new URL(link)
: new URL(link, window.location.origin);
applyNextParam(finalURL);
finalURL.searchParams.set("inspector", "open");
window.open(finalURL, "_blank");
})
.catch(async (error: unknown) => {
if (isResponseErrorLike(error)) {
// This request can return a HTTP 400 when a flow
// is not applicable.
window.open(error.response.url, "_blank");
}
});
};
render(): TemplateResult {
if (!this.flow) {
return html``;
@@ -157,61 +198,22 @@ export class FlowViewPage extends AKElement {
<button
class="pf-c-button pf-m-block pf-m-primary"
@click=${() => {
const finalURL = `${
window.location.origin
}/if/flow/${this.flow.slug}/${AndNext(
`${window.location.pathname}#${window.location.hash}`,
)}`;
window.open(finalURL, "_blank");
const url = formatFlowURL(this.flow);
window.open(url, "_blank");
}}
>
${msg("Normal")}
</button>
<button
class="pf-c-button pf-m-block pf-m-secondary"
@click=${() => {
new FlowsApi(DEFAULT_CONFIG)
.flowsInstancesExecuteRetrieve({
slug: this.flow.slug,
})
.then((link) => {
const finalURL = `${
link.link
}${AndNext(
`${window.location.pathname}#${window.location.hash}`,
)}`;
window.open(finalURL, "_blank");
});
}}
@click=${this.#executeFlow}
>
${msg("with current user")}
</button>
<button
class="pf-c-button pf-m-block pf-m-secondary"
@click=${() => {
new FlowsApi(DEFAULT_CONFIG)
.flowsInstancesExecuteRetrieve({
slug: this.flow.slug,
})
.then((link) => {
const finalURL = `${
link.link
}?${encodeURI(
`inspector=open&next=/#${window.location.hash}`,
)}`;
window.open(finalURL, "_blank");
})
.catch(async (error: unknown) => {
if (isResponseErrorLike(error)) {
// This request can return a HTTP 400 when a flow
// is not applicable.
window.open(
error.response.url,
"_blank",
);
}
});
}}
@click=${this.#executeFlowInspector}
>
${msg("with inspector")}
</button>

View File

@@ -43,3 +43,51 @@ export function LayoutToLabel(layout: FlowLayoutEnum): string {
return msg("Unknown layout");
}
}
/**
* Applies the next URL as a query parameter to the given URL or URLSearchParams object.
*
* @todo deprecate this once hash routing is removed.
*/
export function applyNextParam(
target: URL | URLSearchParams,
destination: string | URL = window.location.pathname + "#" + window.location.hash,
): void {
const searchParams = target instanceof URL ? target.searchParams : target;
searchParams.set("next", destination.toString());
}
/**
* Creates a URLSearchParams object with the next URL as a query parameter.
*
* @todo deprecate this once hash routing is removed.
*/
export function createNextSearchParams(
destination: string | URL = window.location.pathname + "#" + window.location.hash,
): URLSearchParams {
const searchParams = new URLSearchParams();
applyNextParam(searchParams, destination);
return searchParams;
}
/**
* Creates a URL to a flow, with the next URL as a query parameter.
*
* @param flow The flow to create the URL for.
* @param destination The next URL to redirect to after the flow is completed, `true` to use the current route.
*/
export function formatFlowURL(
flow: Flow,
destination: string | URL | null = window.location.pathname + "#" + window.location.hash,
): URL {
const url = new URL(`/if/flow/${flow.slug}/`, window.location.origin);
if (destination) {
applyNextParam(url, destination);
}
return url;
}

View File

@@ -10,6 +10,8 @@ import { DEFAULT_CONFIG } from "#common/api/config";
import { PaginatedResponse, TableColumn } from "#elements/table/Table";
import { TablePage } from "#elements/table/TablePage";
import { toIdentityGroup } from "#admin/navigation";
import { CoreApi, Group } from "@goauthentik/api";
import { msg } from "@lit/localize";
@@ -77,7 +79,7 @@ export class GroupListPage extends TablePage<Group> {
row(item: Group): TemplateResult[] {
return [
html`<a href="#/identity/groups/${item.pk}">${item.name}</a>`,
html`<a href="${toIdentityGroup(item.pk)}">${item.name}</a>`,
html`${item.parentName || msg("-")}`,
html`${Array.from(item.users || []).length}`,
html`<ak-status-label type="info" ?good=${item.isSuperuser}></ak-status-label>`,

View File

@@ -12,6 +12,8 @@ import { DEFAULT_CONFIG } from "#common/api/config";
import { Form } from "#elements/forms/Form";
import { PaginatedResponse, Table, TableColumn } from "#elements/table/Table";
import { toIdentityGroup } from "#admin/navigation";
import { CoreApi, Group, User } from "@goauthentik/api";
import { msg, str } from "@lit/localize";
@@ -142,7 +144,7 @@ export class RelatedGroupList extends Table<Group> {
row(item: Group): TemplateResult[] {
return [
html`<a href="#/identity/groups/${item.pk}">${item.name}</a>`,
html`<a href="${toIdentityGroup(item.pk)}">${item.name}</a>`,
html`${item.parentName || msg("-")}`,
html`<ak-label type="info" ?good=${item.isSuperuser}></ak-label>`,
html` <ak-forms-modal>

View File

@@ -23,10 +23,12 @@ import { Form } from "#elements/forms/Form";
import { showMessage } from "#elements/messages/MessageContainer";
import { WithBrandConfig } from "#elements/mixins/branding";
import { CapabilitiesEnum, WithCapabilitiesConfig } from "#elements/mixins/capabilities";
import { getURLParam, updateURLParams } from "#elements/router/RouteMatch";
import { getURLParam, updateURLParams } from "#elements/router/navigation";
import { PaginatedResponse, Table, TableColumn } from "#elements/table/Table";
import { UserOption } from "#elements/user/utils";
import { toIdentityUser } from "#admin/navigation";
import { CoreApi, CoreUsersListTypeEnum, Group, SessionUser, User } from "@goauthentik/api";
import { msg, str } from "@lit/localize";
@@ -119,7 +121,7 @@ export class RelatedUserList extends WithBrandConfig(WithCapabilitiesConfig(Tabl
order = "last_login";
@property({ type: Boolean })
hideServiceAccounts = getURLParam<boolean>("hideServiceAccounts", true);
hideServiceAccounts = !!(getURLParam("hideServiceAccounts") ?? true);
@state()
me?: SessionUser;
@@ -184,7 +186,7 @@ export class RelatedUserList extends WithBrandConfig(WithCapabilitiesConfig(Tabl
const canImpersonate =
this.can(CapabilitiesEnum.CanImpersonate) && item.pk !== this.me?.user.pk;
return [
html`<a href="#/identity/users/${item.pk}">
html`<a href="${toIdentityUser(item.pk)}">
<div>${item.username}</div>
<small>${item.name}</small>
</a>`,

View File

@@ -0,0 +1,75 @@
import { toAdminRoute } from "#elements/router/navigation";
export const toAdministrationDashboardUsers = () => toAdminRoute("/administration/dashboard/users");
export const toAdministrationOverview = () => toAdminRoute("/administration/overview");
export const toAdministrationSystemTasks = () => toAdminRoute("/administration/system-tasks");
export const toAdminSettings = () => toAdminRoute("/admin/settings");
export const toApplication = (slug: string) => toAdminRoute(`/core/applications/${slug}`);
export const toApplications = (params?: { createWizard?: boolean }) => {
return toAdminRoute(`/core/applications`, params);
};
export const toBlueprintsInstances = () => toAdminRoute("/blueprints/instances");
export const toBrands = () => toAdminRoute("/core/brands");
export const toPropertyMappings = () => toAdminRoute("/core/property-mappings");
export const toSources = () => toAdminRoute("/core/sources");
export const toTokens = () => toAdminRoute("/core/tokens");
export const toCryptoCertificates = () => toAdminRoute("/crypto/certificates");
export const toEnterpriseLicenses = () => toAdminRoute("/enterprise/licenses");
export const toEventLog = (id: string) => toAdminRoute(`/events/log/${id}`);
export const toEventLogs = () => toAdminRoute("/events/log");
export const toEventsRules = () => toAdminRoute("/events/rules");
export const toEventsTransports = () => toAdminRoute("/events/transports");
export const toFlow = (slug: string) => toAdminRoute(`/flow/flows/${slug}`);
export const toFlows = () => toAdminRoute("/flow/flows");
export const toFlowStages = () => toAdminRoute("/flow/stages");
export const toFlowStagesInvitations = () => toAdminRoute("/flow/stages/invitations");
export const toFlowStagesPrompts = () => toAdminRoute("/flow/stages/prompts");
export const toIdentityGroup = (id: string) => toAdminRoute(`/identity/groups/${id}`);
export const toIdentityGroups = () => toAdminRoute("/identity/groups");
export const toIdentityInitialPermissions = () => toAdminRoute("/identity/initial-permissions");
export const toIdentityRole = (id: string) => toAdminRoute(`/identity/roles/${id}`);
export const toIdentityRoles = () => toAdminRoute("/identity/roles");
export const toIdentityUser = (id: number) => toAdminRoute(`/identity/users/${id}`);
export const toIdentityUsers = () => toAdminRoute("/identity/users");
export const toOutpostIntegrations = () => toAdminRoute("/outpost/integrations");
export const toOutposts = () => toAdminRoute("/outpost/outposts");
export const toPolicyPolicies = () => toAdminRoute("/policy/policies");
export const toPolicyReputation = () => toAdminRoute("/policy/reputation");
export const toProvider = (id: number) => toAdminRoute(`/core/providers/${id}`);
export const toProviders = () => toAdminRoute(`/core/providers`);
export const toSource = (slug: string) => toAdminRoute(`/core/sources/${slug}`);

View File

@@ -15,6 +15,8 @@ import { PFColor } from "#elements/Label";
import { PaginatedResponse, TableColumn } from "#elements/table/Table";
import { TablePage } from "#elements/table/TablePage";
import { toProvider } from "#admin/navigation";
import {
Outpost,
OutpostHealth,
@@ -121,7 +123,7 @@ export class OutpostListPage extends TablePage<Outpost> {
html`<ul>
${item.providersObj?.map((p) => {
return html`<li>
<a href="#/core/providers/${p.pk}">${p.name}</a>
<a href="${toProvider(p.pk)}">${p.name}</a>
</li>`;
})}
</ul>`,

View File

@@ -22,7 +22,7 @@ import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
import { DEFAULT_CONFIG } from "#common/api/config";
import { getURLParam, updateURLParams } from "#elements/router/RouteMatch";
import { getURLParam, updateURLParams } from "#elements/router/navigation";
import { PaginatedResponse, TableColumn } from "#elements/table/Table";
import { TablePage } from "#elements/table/TablePage";
@@ -55,7 +55,7 @@ export class PropertyMappingListPage extends TablePage<PropertyMapping> {
order = "name";
@state()
hideManaged = getURLParam<boolean>("hideManaged", true);
hideManaged = !!(getURLParam("hideManaged") ?? true);
async apiEndpoint(): Promise<PaginatedResponse<PropertyMapping>> {
return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsAllList({

View File

@@ -21,6 +21,8 @@ import { DEFAULT_CONFIG } from "#common/api/config";
import { PaginatedResponse, TableColumn } from "#elements/table/Table";
import { TablePage } from "#elements/table/TablePage";
import { toApplication, toProvider } from "#admin/navigation";
import { Provider, ProvidersApi } from "@goauthentik/api";
import { msg, str } from "@lit/localize";
@@ -89,14 +91,14 @@ export class ProviderListPage extends TablePage<Provider> {
if (item.assignedApplicationName) {
return html`<i class="pf-icon pf-icon-ok pf-m-success"></i>
${msg("Assigned to application ")}
<a href="#/core/applications/${item.assignedApplicationSlug}"
<a href="${toApplication(item.assignedApplicationSlug)}"
>${item.assignedApplicationName}</a
>`;
}
if (item.assignedBackchannelApplicationName) {
return html`<i class="pf-icon pf-icon-ok pf-m-success"></i>
${msg("Assigned to application (backchannel) ")}
<a href="#/core/applications/${item.assignedBackchannelApplicationSlug}"
<a href="${toApplication(item.assignedBackchannelApplicationSlug)}"
>${item.assignedBackchannelApplicationName}</a
>`;
}
@@ -107,7 +109,7 @@ export class ProviderListPage extends TablePage<Provider> {
row(item: Provider): TemplateResult[] {
return [
html`<a href="#/core/providers/${item.pk}"> ${item.name} </a>`,
html`<a href=${toProvider(item.pk)}>${item.name}</a>`,
this.rowApp(item),
html`${item.verboseName}`,
html`<ak-forms-modal>

View File

@@ -4,6 +4,8 @@ import "#elements/forms/ModalForm";
import { AKElement } from "#elements/Base";
import { toApplication } from "#admin/navigation";
import { Provider } from "@goauthentik/api";
import { msg } from "@lit/localize";
@@ -25,13 +27,13 @@ export class RelatedApplicationButton extends AKElement {
render(): TemplateResult {
if (this.mode === "primary" && this.provider?.assignedApplicationSlug) {
return html`<a href="#/core/applications/${this.provider.assignedApplicationSlug}">
return html`<a href="${toApplication(this.provider.assignedApplicationSlug)}">
${this.provider.assignedApplicationName}
</a>`;
}
if (this.mode === "backchannel" && this.provider?.assignedBackchannelApplicationSlug) {
return html`<a
href="#/core/applications/${this.provider.assignedBackchannelApplicationSlug}"
href="${toApplication(this.provider.assignedBackchannelApplicationSlug)}"
>
${this.provider.assignedBackchannelApplicationName}
</a>`;

View File

@@ -6,6 +6,8 @@ import { DEFAULT_CONFIG } from "#common/api/config";
import { PaginatedResponse, Table, TableColumn } from "#elements/table/Table";
import { toIdentityGroup } from "#admin/navigation";
import {
GoogleWorkspaceProviderGroup,
ProvidersApi,
@@ -81,7 +83,7 @@ export class GoogleWorkspaceProviderGroupList extends Table<GoogleWorkspaceProvi
row(item: GoogleWorkspaceProviderGroup): TemplateResult[] {
return [
html`<a href="#/identity/groups/${item.groupObj.pk}">
html`<a href="${toIdentityGroup(item.groupObj.pk)}">
<div>${item.groupObj.name}</div>
</a>`,
html`${item.id}`,

View File

@@ -6,6 +6,8 @@ import { DEFAULT_CONFIG } from "#common/api/config";
import { PaginatedResponse, Table, TableColumn } from "#elements/table/Table";
import { toIdentityUser } from "#admin/navigation";
import {
GoogleWorkspaceProviderUser,
ProvidersApi,
@@ -81,7 +83,7 @@ export class GoogleWorkspaceProviderUserList extends Table<GoogleWorkspaceProvid
row(item: GoogleWorkspaceProviderUser): TemplateResult[] {
return [
html`<a href="#/identity/users/${item.userObj.pk}">
html`<a href="${toIdentityUser(item.userObj.pk)}">
<div>${item.userObj.username}</div>
<small>${item.userObj.name}</small>
</a>`,

View File

@@ -6,6 +6,8 @@ import { DEFAULT_CONFIG } from "#common/api/config";
import { PaginatedResponse, Table, TableColumn } from "#elements/table/Table";
import { toIdentityGroup } from "#admin/navigation";
import {
MicrosoftEntraProviderGroup,
ProvidersApi,
@@ -78,7 +80,7 @@ export class MicrosoftEntraProviderGroupList extends Table<MicrosoftEntraProvide
row(item: MicrosoftEntraProviderGroup): TemplateResult[] {
return [
html`<a href="#/identity/groups/${item.groupObj.pk}">
html`<a href="${toIdentityGroup(item.groupObj.pk)}">
<div>${item.groupObj.name}</div>
</a>`,
html`${item.id}`,

View File

@@ -6,6 +6,8 @@ import { DEFAULT_CONFIG } from "#common/api/config";
import { PaginatedResponse, Table, TableColumn } from "#elements/table/Table";
import { toIdentityUser } from "#admin/navigation";
import {
MicrosoftEntraProviderUser,
ProvidersApi,
@@ -81,7 +83,7 @@ export class MicrosoftEntraProviderUserList extends Table<MicrosoftEntraProvider
row(item: MicrosoftEntraProviderUser): TemplateResult[] {
return [
html`<a href="#/identity/users/${item.userObj.pk}">
html`<a href="${toIdentityUser(item.userObj.pk)}">
<div>${item.userObj.username}</div>
<small>${item.userObj.name}</small>
</a>`,

View File

@@ -14,7 +14,7 @@ import { EVENT_REFRESH } from "#common/constants";
import type { Replacer } from "#elements/ak-mdx/index";
import { AKElement } from "#elements/Base";
import { getURLParam } from "#elements/router/RouteMatch";
import { getURLParam } from "#elements/router/navigation";
import { formatSlug } from "#elements/router/utils";
import {
@@ -157,7 +157,7 @@ export class ProxyProviderViewPage extends AKElement {
(input: string): string => {
// The generated config is pretty unreliable currently so
// put it behind a flag
if (!getURLParam("generatedConfig", false)) {
if (!getURLParam("generatedConfig")) {
return input;
}
if (!this.provider) {

View File

@@ -6,6 +6,8 @@ import { DEFAULT_CONFIG } from "#common/api/config";
import { PaginatedResponse, Table, TableColumn } from "#elements/table/Table";
import { toIdentityGroup } from "#admin/navigation";
import {
ProvidersApi,
ProvidersScimSyncObjectCreateRequest,
@@ -78,7 +80,7 @@ export class SCIMProviderGroupList extends Table<SCIMProviderGroup> {
row(item: SCIMProviderGroup): TemplateResult[] {
return [
html`<a href="#/identity/groups/${item.groupObj.pk}">
html`<a href="${toIdentityGroup(item.groupObj.pk)}">
<div>${item.groupObj.name}</div>
</a>`,
html`${item.id}`,

View File

@@ -6,6 +6,8 @@ import { DEFAULT_CONFIG } from "#common/api/config";
import { PaginatedResponse, Table, TableColumn } from "#elements/table/Table";
import { toIdentityUser } from "#admin/navigation";
import {
ProvidersApi,
ProvidersScimSyncObjectCreateRequest,
@@ -78,7 +80,7 @@ export class SCIMProviderUserList extends Table<SCIMProviderUser> {
row(item: SCIMProviderUser): TemplateResult[] {
return [
html`<a href="#/identity/users/${item.userObj.pk}">
html`<a href="${toIdentityUser(item.userObj.pk)}">
<div>${item.userObj.username}</div>
<small>${item.userObj.name}</small>
</a>`,

View File

@@ -7,6 +7,8 @@ import { DEFAULT_CONFIG } from "#common/api/config";
import { PaginatedResponse, Table, TableColumn } from "#elements/table/Table";
import { toIdentityRole } from "#admin/navigation";
import {
PaginatedPermissionList,
RbacApi,
@@ -108,7 +110,7 @@ export class RoleAssignedObjectPermissionTable extends Table<RoleAssignedObjectP
}
row(item: RoleAssignedObjectPermission): TemplateResult[] {
const baseRow = [html` <a href="#/identity/roles/${item.rolePk}">${item.name}</a>`];
const baseRow = [html` <a href="${toIdentityRole(item.rolePk)}">${item.name}</a>`];
this.modelPermissions?.results.forEach((perm) => {
const granted =
item.permissions.filter((uperm) => uperm.codename === perm.codename).length > 0;

View File

@@ -7,6 +7,8 @@ import { DEFAULT_CONFIG } from "#common/api/config";
import { PaginatedResponse, Table, TableColumn } from "#elements/table/Table";
import { toIdentityUser } from "#admin/navigation";
import {
PaginatedPermissionList,
RbacApi,
@@ -113,7 +115,7 @@ export class UserAssignedObjectPermissionTable extends Table<UserAssignedObjectP
}
row(item: UserAssignedObjectPermission): TemplateResult[] {
const baseRow = [html` <a href="#/identity/users/${item.pk}"> ${item.username} </a> `];
const baseRow = [html`<a href="${toIdentityUser(item.pk)}">${item.username}</a>`];
this.modelPermissions?.results.forEach((perm) => {
let cell = html`<i class="fas fa-times pf-m-danger"></i>`;
if (item.permissions.filter((uperm) => uperm.codename === perm.codename).length > 0) {

View File

@@ -9,6 +9,8 @@ import { DEFAULT_CONFIG } from "#common/api/config";
import { PaginatedResponse, TableColumn } from "#elements/table/Table";
import { TablePage } from "#elements/table/TablePage";
import { toIdentityRole } from "#admin/navigation";
import { RbacApi, Role } from "@goauthentik/api";
import { msg } from "@lit/localize";
@@ -80,7 +82,7 @@ export class RoleListPage extends TablePage<Role> {
row(item: Role): TemplateResult[] {
return [
html`<a href="#/identity/roles/${item.pk}">${item.name}</a>`,
html`<a href="${toIdentityRole(item.pk)}">${item.name}</a>`,
html`<ak-forms-modal>
<span slot="submit"> ${msg("Update")} </span>
<span slot="header"> ${msg("Update Role")} </span>

View File

@@ -15,6 +15,8 @@ import { PFColor } from "#elements/Label";
import { PaginatedResponse, TableColumn } from "#elements/table/Table";
import { TablePage } from "#elements/table/TablePage";
import { toSource } from "#admin/navigation";
import { Source, SourcesApi } from "@goauthentik/api";
import { msg, str } from "@lit/localize";
@@ -87,7 +89,7 @@ export class SourceListPage extends TablePage<Source> {
return this.rowInbuilt(item);
}
return [
html`<a href="#/core/sources/${item.slug}">
html`<a href="${toSource(item.slug)}">
<div>${item.name}</div>
${item.enabled
? html``

View File

@@ -2,6 +2,8 @@ import { DEFAULT_CONFIG } from "#common/api/config";
import { PaginatedResponse, Table, TableColumn } from "#elements/table/Table";
import { toIdentityGroup } from "#admin/navigation";
import { SCIMSourceGroup, SourcesApi } from "@goauthentik/api";
import { msg } from "@lit/localize";
@@ -39,7 +41,7 @@ export class SCIMSourceGroupList extends Table<SCIMSourceGroup> {
row(item: SCIMSourceGroup): TemplateResult[] {
return [
html`<a href="#/identity/groups/${item.groupObj.pk}">
html`<a href="${toIdentityGroup(item.groupObj.pk)}">
<div>${item.groupObj.name}</div>
</a>`,
html`${item.externalId}`,

View File

@@ -2,6 +2,8 @@ import { DEFAULT_CONFIG } from "#common/api/config";
import { PaginatedResponse, Table, TableColumn } from "#elements/table/Table";
import { toIdentityUser } from "#admin/navigation";
import { SCIMSourceUser, SourcesApi } from "@goauthentik/api";
import { msg } from "@lit/localize";
@@ -39,7 +41,7 @@ export class SCIMSourceUserList extends Table<SCIMSourceUser> {
row(item: SCIMSourceUser): TemplateResult[] {
return [
html`<a href="#/identity/users/${item.userObj.pk}">
html`<a href="${toIdentityUser(item.userObj.pk)}">
<div>${item.userObj.username}</div>
<small>${item.userObj.name}</small>
</a>`,

View File

@@ -35,6 +35,8 @@ import { DEFAULT_CONFIG } from "#common/api/config";
import { PaginatedResponse, TableColumn } from "#elements/table/Table";
import { TablePage } from "#elements/table/TablePage";
import { toFlow } from "#admin/navigation";
import { Stage, StagesApi } from "@goauthentik/api";
import { msg, str } from "@lit/localize";
@@ -125,7 +127,7 @@ export class StageListPage extends TablePage<Stage> {
html`<ul class="pf-c-list">
${item.flowSet?.map((flow) => {
return html`<li>
<a href="#/flow/flows/${flow.slug}">
<a href="${toFlow(flow.slug)}">
<code>${flow.slug}</code>
</a>
</li>`;

View File

@@ -6,6 +6,7 @@ import { DEFAULT_CONFIG } from "#common/api/config";
import { PaginatedResponse, Table, TableColumn } from "#elements/table/Table";
import { applicationListStyle } from "#admin/applications/ApplicationListPage";
import { toApplication } from "#admin/navigation";
import { Application, CoreApi, User } from "@goauthentik/api";
@@ -45,7 +46,7 @@ export class UserApplicationTable extends Table<Application> {
name=${item.name}
icon=${ifDefined(item.metaIcon || undefined)}
></ak-app-icon>`,
html`<a href="#/core/applications/${item.slug}">
html`<a href="${toApplication(item.slug)}">
<div>${item.name}</div>
${item.metaPublisher ? html`<small>${item.metaPublisher}</small>` : html``}
</a>`,

View File

@@ -24,12 +24,13 @@ import { me } from "#common/users";
import { showAPIErrorMessage, showMessage } from "#elements/messages/MessageContainer";
import { WithBrandConfig } from "#elements/mixins/branding";
import { CapabilitiesEnum, WithCapabilitiesConfig } from "#elements/mixins/capabilities";
import { getURLParam, updateURLParams } from "#elements/router/RouteMatch";
import { getURLParam, updateURLParams } from "#elements/router/navigation";
import { PaginatedResponse, TableColumn } from "#elements/table/Table";
import { TablePage } from "#elements/table/TablePage";
import { writeToClipboard } from "#elements/utils/writeToClipboard";
import type { AdminInterface } from "#admin/AdminInterface/index.entrypoint";
import { toIdentityUser } from "#admin/navigation";
import { CoreApi, SessionUser, User, UserPath } from "@goauthentik/api";
@@ -110,7 +111,7 @@ export class UserListPage extends WithBrandConfig(WithCapabilitiesConfig(TablePa
activePath;
@state()
hideDeactivated = getURLParam<boolean>("hideDeactivated", false);
hideDeactivated = !!getURLParam("hideDeactivated");
@state()
userPaths?: UserPath;
@@ -129,7 +130,8 @@ export class UserListPage extends WithBrandConfig(WithCapabilitiesConfig(TablePa
constructor() {
super();
const defaultPath = new DefaultUIConfig().defaults.userPath;
this.activePath = getURLParam<string>("path", defaultPath);
this.activePath = getURLParam<string>("path") ?? defaultPath;
uiConfig().then((c) => {
if (c.defaults.userPath !== defaultPath) {
this.activePath = c.defaults.userPath;
@@ -243,7 +245,7 @@ export class UserListPage extends WithBrandConfig(WithCapabilitiesConfig(TablePa
const canImpersonate =
this.can(CapabilitiesEnum.CanImpersonate) && item.pk !== this.me?.user.pk;
return [
html`<a href="#/identity/users/${item.pk}">
html`<a href="${toIdentityUser(item.pk)}">
<div>${item.username}</div>
<small>${item.name ? item.name : html`&lt;${msg("No name set")}&gt;`}</small>
</a>`,

View File

@@ -68,13 +68,6 @@ export const DEFAULT_CONFIG = new Configuration({
],
});
// This is just a function so eslint doesn't complain about
// missing-whitespace-between-attributes or
// unexpected-character-in-attribute-name
export function AndNext(url: string): string {
return `?next=${encodeURIComponent(url)}`;
}
console.debug(
`authentik(early): version ${import.meta.env.AK_VERSION}, apiBase ${DEFAULT_CONFIG.basePath}`,
);

View File

@@ -16,17 +16,6 @@ export const CURRENT_CLASS = "pf-m-current";
//#endregion
//#region Application
/**
* The delimiter used to parse the URL for the current route.
*
* @todo Move this to the ak-router.
*/
export const ROUTE_SEPARATOR = ";";
//#endregion
//#region Events
export const EVENT_REFRESH = "ak-refresh";

View File

@@ -2,6 +2,8 @@ import { DEFAULT_CONFIG } from "#common/api/config";
import { EVENT_LOCALE_REQUEST } from "#common/constants";
import { isResponseErrorLike } from "#common/errors/network";
import { navigate } from "#elements/router/navigation";
import { CoreApi, SessionUser } from "@goauthentik/api";
/**
@@ -74,14 +76,13 @@ export async function me(): Promise<SessionUser> {
if (response.status === 401 || response.status === 403) {
const { pathname, search, hash } = window.location;
const authFlowRedirectURL = new URL(
`/flows/-/default/authentication/`,
window.location.origin,
);
authFlowRedirectURL.searchParams.set("next", `${pathname}${search}${hash}`);
window.location.assign(authFlowRedirectURL);
navigate({
interfaceName: "flow",
pathname: "/-/default/authentication/",
search: {
next: `${pathname}${search}${hash}`,
},
});
}
}

View File

@@ -1,7 +1,7 @@
import { CURRENT_CLASS, EVENT_REFRESH, ROUTE_SEPARATOR } from "#common/constants";
import { CURRENT_CLASS, EVENT_REFRESH } from "#common/constants";
import { AKElement } from "#elements/Base";
import { getURLParams, updateURLParams } from "#elements/router/RouteMatch";
import { getURLParam, updateURLParams } from "#elements/router/navigation";
import { msg } from "@lit/localize";
import { css, CSSResult, html, TemplateResult } from "lit";
@@ -14,13 +14,13 @@ import PFGlobal from "@patternfly/patternfly/patternfly-base.css";
@customElement("ak-tabs")
export class Tabs extends AKElement {
@property()
pageIdentifier = "page";
public pageIdentifier = "page";
@property()
currentPage?: string;
@property({ type: Number })
public currentPage: string | null = null;
@property({ type: Boolean })
vertical = false;
public vertical = false;
static styles: CSSResult[] = [
PFGlobal,
@@ -67,19 +67,23 @@ export class Tabs extends AKElement {
super.disconnectedCallback();
}
onClick(slot?: string): void {
onClick(slot: string | null): void {
this.currentPage = slot;
const params: { [key: string]: string | undefined } = {};
params[this.pageIdentifier] = slot;
updateURLParams(params);
updateURLParams({
[this.pageIdentifier]: slot,
});
const page = this.querySelector(`[slot='${this.currentPage}']`);
if (!page) return;
page.dispatchEvent(new CustomEvent(EVENT_REFRESH));
page.dispatchEvent(new CustomEvent("activate"));
}
renderTab(page: Element): TemplateResult {
const slot = page.attributes.getNamedItem("slot")?.value;
const slot = page.attributes.getNamedItem("slot")?.value || null;
return html` <li class="pf-c-tabs__item ${slot === this.currentPage ? CURRENT_CLASS : ""}">
<button class="pf-c-tabs__link" @click=${() => this.onClick(slot)}>
<span class="pf-c-tabs__item-text"> ${page.getAttribute("data-tab-title")} </span>
@@ -89,24 +93,26 @@ export class Tabs extends AKElement {
render(): TemplateResult {
const pages = Array.from(this.querySelectorAll(":scope > [slot^='page-']"));
if (window.location.hash.includes(ROUTE_SEPARATOR)) {
const params = getURLParams();
if (
this.pageIdentifier in params &&
!this.currentPage &&
this.querySelector(`[slot='${params[this.pageIdentifier]}']`) !== null
) {
// To update the URL to match with the current slot
this.onClick(params[this.pageIdentifier] as string);
}
const pageParam = getURLParam<string>(this.pageIdentifier);
if (
pageParam &&
!this.currentPage &&
this.querySelector(`[slot='${pageParam}']`) !== null
) {
// To update the URL to match with the current slot
this.onClick(pageParam);
}
if (!this.currentPage) {
if (pages.length < 1) {
return html`<h1>${msg("no tabs defined")}</h1>`;
}
const wantedPage = pages[0].attributes.getNamedItem("slot")?.value;
const wantedPage = pages[0].attributes.getNamedItem("slot")?.value || null;
this.onClick(wantedPage);
}
return html`<div class="pf-c-tabs ${this.vertical ? "pf-m-vertical pf-m-box" : ""}">
<ul class="pf-c-tabs__list">
${pages.map((page) => this.renderTab(page))}

View File

@@ -1,7 +1,7 @@
import { EVENT_REFRESH } from "#common/constants";
import { AKElement } from "#elements/Base";
import { setURLParams } from "#elements/router/RouteMatch";
import { navigate } from "#elements/router/navigation";
import { msg } from "@lit/localize";
import { CSSResult, html, TemplateResult } from "lit";
@@ -85,7 +85,13 @@ export class TreeViewNode extends AKElement {
if (this.host) {
this.host.activeNode = this;
}
setURLParams({ path: this.fullPath });
navigate((route) => ({
...route,
params: {
path: this.fullPath,
},
}));
this.dispatchEvent(
new CustomEvent(EVENT_REFRESH, {
bubbles: true,

View File

@@ -1,56 +1,61 @@
import "#elements/EmptyState";
import { html, TemplateResult } from "lit";
import { SlottedTemplateResult } from "#elements/types";
import { html, nothing, TemplateResult } from "lit";
import { until } from "lit/directives/until.js";
export const SLUG_REGEX = "[-a-zA-Z0-9_]+";
export const ID_REGEX = "\\d+";
export const UUID_REGEX = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
export interface RouteArgs {
[key: string]: string;
}
export type URLParameterRecord = Record<string, string>;
export class Route {
url: RegExp;
export type RouteCallback<P extends URLParameterRecord> = (
args: P,
) => Promise<SlottedTemplateResult>;
export class Route<P extends URLParameterRecord = URLParameterRecord> {
public readonly url: RegExp;
private element?: TemplateResult;
private callback?: (args: RouteArgs) => Promise<TemplateResult>;
#callback?: RouteCallback<P>;
constructor(url: RegExp, callback?: (args: RouteArgs) => Promise<TemplateResult>) {
constructor(url: RegExp, callback?: RouteCallback<P>) {
this.url = url;
this.callback = callback;
this.#callback = callback;
}
redirect(to: string, raw = false): Route {
this.callback = async () => {
redirect(to: string, raw = false): Route<P> {
this.#callback = async () => {
console.debug(`authentik/router: redirecting ${to}`);
if (!raw) {
window.location.hash = `#${to}`;
} else {
window.location.hash = to;
}
return html``;
return nothing;
};
return this;
}
then(render: (args: RouteArgs) => TemplateResult): Route {
this.callback = async (args) => {
then(render: (args: P) => TemplateResult): Route<P> {
this.#callback = async (args) => {
return render(args);
};
return this;
}
thenAsync(render: (args: RouteArgs) => Promise<TemplateResult>): Route {
this.callback = render;
thenAsync(render: (args: P) => Promise<TemplateResult>): Route<P> {
this.#callback = render;
return this;
}
render(args: RouteArgs): TemplateResult {
if (this.callback) {
render(args: P): TemplateResult {
if (this.#callback) {
return html`${until(
this.callback(args),
this.#callback(args),
html`<ak-empty-state loading></ak-empty-state>`,
)}`;
}
@@ -61,6 +66,6 @@ export class Route {
}
toString(): string {
return `<Route url=${this.url} callback=${this.callback ? "true" : "false"}>`;
return `<Route url=${this.url} callback=${this.#callback ? "true" : "false"}>`;
}
}

View File

@@ -1,5 +1,3 @@
import { ROUTE_SEPARATOR } from "#common/constants";
import { Route } from "#elements/router/Route";
import { TemplateResult } from "lit";
@@ -40,58 +38,3 @@ export class RouteMatch {
)}>`;
}
}
export function createPathnameHash(
hashRoute?: string | null,
basePath = location.pathname,
): string {
if (!hashRoute) return basePath;
return `${basePath}#${hashRoute}`;
}
export function getURLParam<T>(key: string, fallback: T): T {
const params = getURLParams();
if (key in params) {
return params[key] as T;
}
return fallback;
}
export function getURLParams(): { [key: string]: unknown } {
const params = {};
if (window.location.hash.includes(ROUTE_SEPARATOR)) {
const urlParts = window.location.hash.slice(1, Infinity).split(ROUTE_SEPARATOR, 2);
const rawParams = decodeURIComponent(urlParts[1]);
try {
return JSON.parse(rawParams);
} catch {
return params;
}
}
return params;
}
export function setURLParams(params: { [key: string]: unknown }, replace = true): void {
const serializedParams = JSON.stringify(params);
const [currentRoute] = window.location.hash.slice(1).split(ROUTE_SEPARATOR);
const nextPathname = createPathnameHash(
`${currentRoute};${encodeURIComponent(serializedParams)}`,
);
if (replace) {
history.replaceState(undefined, "", nextPathname);
} else {
history.pushState(undefined, "", nextPathname);
}
}
export function updateURLParams(params: { [key: string]: unknown }, replace = true): void {
const currentParams = getURLParams();
for (const key in params) {
currentParams[key] = params[key] as string;
}
setURLParams(currentParams, replace);
}

View File

@@ -1,8 +1,7 @@
import "#elements/router/Router404";
import { ROUTE_SEPARATOR } from "#common/constants";
import { AKElement } from "#elements/Base";
import { ROUTE_SEPARATOR } from "#elements/router/constants";
import { Route } from "#elements/router/Route";
import { RouteMatch } from "#elements/router/RouteMatch";
@@ -18,41 +17,6 @@ import {
import { css, CSSResult, html, PropertyValues, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators.js";
// Poliyfill for hashchange.newURL,
// https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onhashchange
window.addEventListener("load", () => {
if (!window.HashChangeEvent)
(function () {
let lastURL = document.URL;
window.addEventListener("hashchange", function (event) {
Object.defineProperty(event, "oldURL", {
enumerable: true,
configurable: true,
value: lastURL,
});
Object.defineProperty(event, "newURL", {
enumerable: true,
configurable: true,
value: document.URL,
});
lastURL = document.URL;
});
})();
});
export function paramURL(url: string, params?: { [key: string]: unknown }): string {
let finalUrl = "#";
finalUrl += url;
if (params) {
finalUrl += ";";
finalUrl += encodeURIComponent(JSON.stringify(params));
}
return finalUrl;
}
export function navigate(url: string, params?: { [key: string]: unknown }): void {
window.location.assign(paramURL(url, params));
}
@customElement("ak-router-outlet")
export class RouterOutlet extends AKElement {
@property({ attribute: false })

View File

@@ -0,0 +1,35 @@
/**
* @file Router constants.
*/
export const TITLE_DEFAULT = "authentik";
/**
* Route separator, used to separate the path from the mock query string.
*/
export const ROUTE_SEPARATOR = ";";
/**
* Slug pattern, matching alphanumeric characters, underscores, and hyphens.
*/
export const SLUG_PATTERN = "[a-zA-Z0-9_\\-]+";
/**
* Numeric ID pattern, typically used for database IDs.
*/
export const ID_PATTERN = "\\d+";
/**
* UUID v4 pattern
*
* @todo Enforcing this format on the front-end may be a bit too strict.
* We may want to allow other UUID formats, or move this to a validation step.
*/
export const UUID_PATTERN = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
/**
* The name identifier for the current interface.
*
* @category Routing
*/
export type RouteInterfaceName = "user" | "admin" | "flow" | "unknown";

View File

@@ -0,0 +1,143 @@
import { ROUTE_SEPARATOR, RouteInterfaceName } from "#elements/router/constants";
import {
decodeParameters,
pluckRoute,
PrimitiveRouteParameter,
recordToSearchParams,
RouteParameterRecord,
RouterParameterInit,
} from "#elements/router/parsing";
import { readInterfaceRouteParam } from "#elements/router/utils";
/**
* Create a route hash from a pathname and parameters.
*
* @param pathname The pathname of the route.
* @param params The parameters to serialize.
* @returns The formatted route hash, starting with `#`.
*/
export function paramURL(pathname: string, params?: RouterParameterInit): string {
const routePrefix = "#" + pathname;
if (!params) return routePrefix;
const searchParams = recordToSearchParams(params);
return [routePrefix, searchParams.toString()].join(ROUTE_SEPARATOR);
}
export function updateURLParams(params: RouteParameterRecord, replace = true): void {
navigate((route) => ({
...route,
params: {
...route.params,
...params,
},
}));
}
export function getURLParam<
T extends PrimitiveRouteParameter | PrimitiveRouteParameter[] =
| PrimitiveRouteParameter
| PrimitiveRouteParameter[],
>(key: string) {
const params = decodeParameters();
return params[key] as T;
}
export interface RouteState {
interfaceName: RouteInterfaceName;
pathname: string;
params: RouteParameterRecord;
search?: RouteParameterRecord;
}
export type To = URL | string | Partial<RouteState> | ((currentRoute: RouteState) => RouteState);
export interface NavigateOptions {
mode?: "push" | "replace" | "assign";
}
/**
* Navigate to a route.
*
* @param {string} pathname The pathname of the route.
* @param {RouteParameterRecord} params The parameters to serialize.
*/
export function navigate(to?: To, { mode = "replace" }: NavigateOptions = {}): void {
if (!to) {
console.warn("authentik/router: no destination provided, aborting navigation");
return;
}
let next: URL;
if (typeof to === "string" || to instanceof URL) {
next = URL.canParse(to) ? new URL(to) : new URL(to, window.location.origin);
} else if (typeof to === "function") {
const nextRoute = to({
interfaceName: readInterfaceRouteParam(),
params: decodeParameters(),
pathname: pluckRoute().pathname,
});
next = new URL(formatInterfaceRoute(nextRoute), window.location.origin);
} else {
next = new URL(
formatInterfaceRoute({
interfaceName: readInterfaceRouteParam(),
...to,
}),
window.location.origin,
);
}
if (mode === "assign") {
console.debug(`authentik/router: Assigning ${next.href}`);
return window.location.assign(next);
}
if (next.href === window.location.href) {
// Nothing to do.
return;
}
console.debug(`authentik/router: (${mode}) navigating to ${next.href}`);
if (mode === "replace") {
return history.replaceState(undefined, "", next);
}
return history.pushState(undefined, "", next);
}
/**
* Create a route to an interface by name, optionally with parameters.
*/
export function formatInterfaceRoute({
interfaceName = readInterfaceRouteParam(),
pathname = "/",
params,
search,
}: Partial<RouteState>): string {
let pathBuilder = new URL(document.baseURI).pathname + `if/${interfaceName}/`;
if (search) {
pathBuilder += `?${recordToSearchParams(search).toString()}`;
}
return pathBuilder + paramURL(pathname, params);
}
export function toUserRoute(pathname: string, params?: RouteParameterRecord) {
return formatInterfaceRoute({ interfaceName: "user", pathname, params });
}
export function toAdminRoute(pathname: string, params?: RouteParameterRecord) {
return formatInterfaceRoute({ interfaceName: "admin", pathname, params });
}
export function toFlowRoute(pathname: string, params?: RouteParameterRecord) {
return formatInterfaceRoute({ interfaceName: "flow", pathname, params });
}

View File

@@ -0,0 +1,140 @@
import { ROUTE_SEPARATOR } from "#elements/router/constants";
export type PrimitiveRouteParameter = string | number | boolean | null | undefined;
export type RouteParameterRecord = Record<
string,
PrimitiveRouteParameter | PrimitiveRouteParameter[]
>;
export type RouterParameterInit = RouteParameterRecord | URLSearchParams;
//#region Serialization
function serialize(value: PrimitiveRouteParameter): string | null {
if (!value) return null;
if (typeof value === "boolean") {
return value ? "true" : null;
}
return value.toString();
}
/**
* Given a record of parameters, create a URLSearchParams object.
*/
export function recordToSearchParams(params: RouterParameterInit): URLSearchParams {
if (params instanceof URLSearchParams) {
return params;
}
const searchParams = new URLSearchParams();
for (const [key, value] of Object.entries(params)) {
if (Array.isArray(value)) {
for (const item of value) {
const serialized = serialize(item);
if (!serialized) continue;
searchParams.append(key, serialized);
}
continue;
}
const serializedValue = serialize(value);
if (!serializedValue) continue;
searchParams.set(key, serializedValue);
}
return searchParams;
}
//#endregion
//#region Deserialization
function deserialize(value: string | null): PrimitiveRouteParameter | null {
if (!value) return null;
if (value === "true") {
return true;
}
if (/^\d+$/.test(value)) {
return parseInt(value, 10);
}
return value;
}
/**
* Given a URLSearchParams object, create a record of parameters.
*
* Keys that appear multiple times in the search params will be mapped to an array of values.
*
* @param searchParams The URLSearchParams object to parse.
*/
export function searchParamsToRecord<T extends RouteParameterRecord = RouteParameterRecord>(
searchParams: URLSearchParams,
): T {
const params: Record<string, unknown> = {};
for (const [key, value] of searchParams.entries()) {
if (params[key]) {
if (Array.isArray(params[key])) {
params[key].push(deserialize(value));
} else {
params[key] = [params[key], deserialize(value)];
}
} else {
params[key] = deserialize(value);
}
}
return params as T;
}
//#endregion
//#region Decoding
export interface SerializedRoute {
pathname: string;
serializedParameters?: string;
}
export function pluckRoute(source: Pick<URL, "hash"> | string = window.location): SerializedRoute {
source = typeof source === "string" ? new URL(source) : source;
const [pathname, serializedParameters] = source.hash.slice(1).split(ROUTE_SEPARATOR, 2);
return {
pathname,
serializedParameters,
};
}
export function decodeParameters<T extends RouteParameterRecord = RouteParameterRecord>(
source: Pick<URL, "hash"> = window.location,
): Partial<T> {
if (!source.hash.includes(ROUTE_SEPARATOR)) {
return {};
}
const { serializedParameters } = pluckRoute(source);
if (!serializedParameters) {
return {};
}
if (serializedParameters.startsWith("%7B")) {
try {
return JSON.parse(decodeURIComponent(serializedParameters));
} catch {
return {};
}
}
return searchParamsToRecord<T>(new URLSearchParams(serializedParameters));
}

View File

@@ -2,14 +2,9 @@
* @file Utilities for working with the client-side page router.
*/
import { kebabCase } from "change-case";
import { RouteInterfaceName } from "#elements/router/constants";
/**
* The name identifier for the current interface.
*
* @category Routing
*/
export type RouteInterfaceName = "user" | "admin" | "flow" | "unknown";
import { kebabCase } from "change-case";
/**
* Read the current interface route parameter from the URL.

View File

@@ -1,7 +1,5 @@
import { ROUTE_SEPARATOR } from "#common/constants";
import { AKElement } from "#elements/Base";
import { createPathnameHash } from "#elements/router/RouteMatch";
import { ROUTE_SEPARATOR } from "#elements/router/constants";
import { msg, str } from "@lit/localize";
import { css, CSSResult, html, nothing, TemplateResult } from "lit";
@@ -215,7 +213,7 @@ export class SidebarItem extends AKElement {
renderWithPath() {
return html`
<a
href=${createPathnameHash(this.path)}
href=${this.path}
id="sidebar-nav-link-${this.path}"
class="pf-c-nav__link ${this.current ? "pf-m-current" : ""}"
aria-current=${ifDefined(this.current ? "page" : undefined)}

View File

@@ -12,7 +12,7 @@ import { groupBy } from "#common/utils";
import { AKElement } from "#elements/Base";
import { WithLicenseSummary } from "#elements/mixins/license";
import { getURLParam, updateURLParams } from "#elements/router/RouteMatch";
import { getURLParam, updateURLParams } from "#elements/router/navigation";
import { SlottedTemplateResult } from "#elements/types";
import { Pagination } from "@goauthentik/api";
@@ -129,7 +129,7 @@ export abstract class Table<T> extends WithLicenseSummary(AKElement) implements
data?: PaginatedResponse<T>;
@property({ type: Number })
page = getURLParam(this.#pageParam, 1);
page = getURLParam<number>(this.#pageParam) ?? 1;
/**
* Set if your `selectedElements` use of the selection box is to enable bulk-delete,
@@ -219,7 +219,7 @@ export abstract class Table<T> extends WithLicenseSummary(AKElement) implements
await this.fetch();
});
if (this.searchEnabled()) {
this.search = getURLParam(this.#searchParam, "");
this.search = getURLParam(this.#searchParam) ?? "";
}
}

View File

@@ -1,6 +1,6 @@
import "#components/ak-page-header";
import { updateURLParams } from "#elements/router/RouteMatch";
import { updateURLParams } from "#elements/router/navigation";
import { Table } from "#elements/table/Table";
import { SlottedTemplateResult } from "#elements/types";

View File

@@ -1,6 +1,6 @@
import "#elements/Spinner";
import { AndNext, DEFAULT_CONFIG } from "#common/api/config";
import { DEFAULT_CONFIG } from "#common/api/config";
import { EVENT_REFRESH } from "#common/constants";
import { parseAPIResponseError, pluckErrorDetail } from "#common/errors/network";
import { MessageLevel } from "#common/messages";
@@ -8,6 +8,10 @@ import { MessageLevel } from "#common/messages";
import { showMessage } from "#elements/messages/MessageContainer";
import { BaseUserSettings } from "#elements/user/sources/BaseUserSettings";
import { toUserSettings } from "#user/navigation";
import { applyNextParam } from "#admin/flows/utils";
import { SourcesApi } from "@goauthentik/api";
import { msg, str } from "@lit/localize";
@@ -64,12 +68,13 @@ export class SourceSettingsOAuth extends BaseUserSettings {
</button>`;
}
if (this.configureUrl) {
return html`<a
class="pf-c-button pf-m-primary"
href="${this.configureUrl}${AndNext(
`/if/user/#/settings;${JSON.stringify({ page: "page-sources" })}`,
)}"
>
const target = new URL(this.configureUrl);
const destination = toUserSettings();
applyNextParam(target, destination);
return html`<a class="pf-c-button pf-m-primary" href="${target}">
${msg("Connect")}
</a>`;
}

View File

@@ -1,6 +1,6 @@
import "#elements/Spinner";
import { AndNext, DEFAULT_CONFIG } from "#common/api/config";
import { DEFAULT_CONFIG } from "#common/api/config";
import { EVENT_REFRESH } from "#common/constants";
import { parseAPIResponseError, pluckErrorDetail } from "#common/errors/network";
import { MessageLevel } from "#common/messages";
@@ -8,6 +8,10 @@ import { MessageLevel } from "#common/messages";
import { showMessage } from "#elements/messages/MessageContainer";
import { BaseUserSettings } from "#elements/user/sources/BaseUserSettings";
import { toUserSettings } from "#user/navigation";
import { applyNextParam } from "#admin/flows/utils";
import { SourcesApi } from "@goauthentik/api";
import { msg, str } from "@lit/localize";
@@ -64,12 +68,13 @@ export class SourceSettingsSAML extends BaseUserSettings {
</button>`;
}
if (this.configureUrl) {
return html`<a
class="pf-c-button pf-m-primary"
href="${this.configureUrl}${AndNext(
`/if/user/#/settings;${JSON.stringify({ page: "page-sources" })}`,
)}"
>
const target = new URL(this.configureUrl);
const destination = toUserSettings({ page: "sources" });
applyNextParam(target, destination);
return html`<a class="pf-c-button pf-m-primary" href="${target}">
${msg("Connect")}
</a>`;
}

View File

@@ -6,6 +6,7 @@ import { parseAPIResponseError } from "#common/errors/network";
import { PlexAPIClient, popupCenterScreen } from "#common/helpers/plex";
import { showAPIErrorMessage } from "#elements/messages/MessageContainer";
import { navigate } from "#elements/router/navigation";
import { BaseStage } from "#flow/stages/base";
@@ -59,14 +60,18 @@ export class PlexLoginInit extends BaseStage<
slug: this.challenge?.slug || "",
})
.then((redirectChallenge) => {
window.location.assign(redirectChallenge.to);
navigate(redirectChallenge.to, {
mode: "assign",
});
})
.catch(async (error: unknown) => {
.catch((error: unknown) => {
return parseAPIResponseError(error)
.then(showAPIErrorMessage)
.then(() => {
setTimeout(() => {
window.location.assign("/");
navigate("/", {
mode: "assign",
});
}, 5000);
});
});

View File

@@ -1,5 +1,7 @@
import "#flow/components/ak-flow-card";
import { navigate } from "#elements/router/navigation";
import { BaseStage } from "#flow/stages/base";
import { FlowChallengeResponseRequest, RedirectChallenge } from "@goauthentik/api";
@@ -58,7 +60,11 @@ export class RedirectStage extends BaseStage<RedirectChallenge, FlowChallengeRes
"authentik/stages/redirect: redirecting to url from server",
this.challenge.to,
);
window.location.assign(this.challenge.to);
navigate(this.challenge.to, {
mode: "assign",
});
this.startedRedirect = true;
}

View File

@@ -1,5 +1,6 @@
import { DEFAULT_CONFIG } from "#common/api/config";
import { navigate } from "#elements/router/navigation";
import { PaginatedResponse, TableColumn } from "#elements/table/Table";
import { TableModal } from "#elements/table/TableModal";
@@ -23,7 +24,9 @@ export class RACLaunchEndpointModal extends TableModal<Endpoint> {
if (this.app?.openInNewTab) {
window.open(item.launchUrl);
} else {
window.location.assign(item.launchUrl);
navigate(item.launchUrl, {
mode: "assign",
});
}
};

View File

@@ -1,7 +1,8 @@
import { docLink, globalAK } from "#common/global";
import { docLink } from "#common/global";
import { AKElement } from "#elements/Base";
import { paramURL } from "#elements/router/RouterOutlet";
import { toApplications } from "#admin/navigation";
import { msg } from "@lit/localize";
import { css, html, nothing } from "lit";
@@ -47,15 +48,13 @@ export class LibraryPageApplicationEmptyList
public isAdmin = false;
#renderNewAppButton() {
const href = paramURL("/core/applications", {
const href = toApplications({
createWizard: true,
});
return html`
<div class="pf-u-pt-lg">
<a
aria-disabled="false"
class="cta pf-c-button pf-m-secondary"
href="${globalAK().api.base}if/admin/${href}"
<a aria-disabled="false" class="cta pf-c-button pf-m-secondary" href="${href}"
>${msg("Create a new application")}</a
>
</div>

View File

@@ -6,7 +6,7 @@ import {
} from "./events.js";
import { AKElement } from "#elements/Base";
import { getURLParam, updateURLParams } from "#elements/router/RouteMatch";
import { getURLParam, navigate, updateURLParams } from "#elements/router/navigation";
import type { Application } from "@goauthentik/api";
@@ -65,7 +65,7 @@ export class LibraryPageApplicationSearch extends AKElement {
}
@property()
query = getURLParam<string | undefined>("search", undefined);
query: string | null = getURLParam("search");
@query("input")
searchInput?: HTMLInputElement;
@@ -112,9 +112,12 @@ export class LibraryPageApplicationSearch extends AKElement {
this.searchInput.value = "";
}
this.query = "";
updateURLParams({
search: this.query,
});
navigate((path) => ({
...path,
params: {
search: this.query,
},
}));
this.dispatchEvent(new LibraryPageSearchReset());
}

View File

@@ -19,6 +19,7 @@ import { groupBy } from "#common/utils";
import { AKElement } from "#elements/Base";
import { bound } from "#elements/decorators/bound";
import { navigate } from "#elements/router/navigation";
import type { Application } from "@goauthentik/api";
@@ -119,7 +120,9 @@ export class LibraryPage extends AKElement {
return;
}
if (!this.selectedApp.openInNewTab) {
window.location.assign(this.selectedApp?.launchUrl);
navigate(this.selectedApp?.launchUrl, {
mode: "assign",
});
} else {
window.open(this.selectedApp.launchUrl);
}

View File

@@ -25,7 +25,7 @@ import { WebsocketClient } from "#common/ws";
import { AuthenticatedInterface } from "#elements/AuthenticatedInterface";
import { AKElement } from "#elements/Base";
import { WithBrandConfig } from "#elements/mixins/branding";
import { getURLParam, updateURLParams } from "#elements/router/RouteMatch";
import { getURLParam, toUserRoute, updateURLParams } from "#elements/router/navigation";
import { themeImage } from "#elements/utils/images";
import { ROUTES } from "#user/Routes";
@@ -201,7 +201,7 @@ class UserInterfacePresentation extends WithBrandConfig(AKElement) {
</div>
<header class="pf-c-page__header">
<div class="pf-c-page__header-brand">
<a href="#/" class="pf-c-page__header-brand-link">
<a href="${toUserRoute("/")}" class="pf-c-page__header-brand-link">
<img
class="pf-c-brand"
src="${themeImage(this.brandingLogo)}"
@@ -264,10 +264,10 @@ class UserInterfacePresentation extends WithBrandConfig(AKElement) {
@customElement("ak-interface-user")
export class UserInterface extends WithBrandConfig(AuthenticatedInterface) {
@property({ type: Boolean })
notificationDrawerOpen = getURLParam("notificationDrawerOpen", false);
notificationDrawerOpen = !!getURLParam("notificationDrawerOpen");
@state()
apiDrawerOpen = getURLParam("apiDrawerOpen", false);
apiDrawerOpen = !!getURLParam("apiDrawerOpen");
@state()
notificationsCount = 0;

View File

@@ -0,0 +1,7 @@
import { toUserRoute } from "#elements/router/navigation";
type SettingPageTab = "sources" | "mfa" | "details";
export const toUserSettings = (params?: { page: SettingPageTab }) => {
return toUserRoute("settings", params);
};

View File

@@ -1,8 +1,9 @@
import { AndNext } from "#common/api/config";
import { globalAK } from "#common/global";
import { AKElement } from "#elements/Base";
import { toUserSettings } from "#user/navigation";
import { createNextSearchParams } from "#admin/flows/utils";
import { msg } from "@lit/localize";
import { CSSResult, html, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators.js";
@@ -22,17 +23,20 @@ export class UserSettingsPassword extends AKElement {
static styles: CSSResult[] = [PFBase, PFCard, PFButton, PFForm, PFFormControl];
render(): TemplateResult {
const searchParams = createNextSearchParams(
toUserSettings({
page: "details",
}),
);
const href = `${this.configureUrl || ""}?${searchParams}`;
// For this stage we don't need to check for a configureFlow,
// as the stage won't return any UI Elements if no configureFlow is set.
return html`<div class="pf-c-card">
<div class="pf-c-card__title">${msg("Change your password")}</div>
<div class="pf-c-card__body">
<a
href="${ifDefined(this.configureUrl)}${AndNext(
`${globalAK().api.relBase}if/user/#/settings;${JSON.stringify({ page: "page-details" })}`,
)}"
class="pf-c-button pf-m-primary"
>
<a href=${ifDefined(href)} class="pf-c-button pf-m-primary">
${msg("Change password")}
</a>
</div>

View File

@@ -6,14 +6,17 @@ import "#elements/forms/ModalForm";
import "#user/user-settings/mfa/MFADeviceForm";
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
import { AndNext, DEFAULT_CONFIG } from "#common/api/config";
import { globalAK } from "#common/global";
import { DEFAULT_CONFIG } from "#common/api/config";
import { deviceTypeName } from "#common/labels";
import { SentryIgnoredError } from "#common/sentry/index";
import { formatElapsedTime } from "#common/temporal";
import { PaginatedResponse, Table, TableColumn } from "#elements/table/Table";
import { toUserSettings } from "#user/navigation";
import { createNextSearchParams } from "#admin/flows/utils";
import { AuthenticatorsApi, Device, UserSetting } from "@goauthentik/api";
import { msg, str } from "@lit/localize";
@@ -73,15 +76,16 @@ export class MFADevicesPage extends Table<Device> {
</button>
<ul class="pf-c-dropdown__menu" hidden>
${settings.map((stage) => {
const searchParams = createNextSearchParams(
toUserSettings({
page: "mfa",
}),
);
const href = `${stage.configureUrl || ""}?${searchParams}`;
return html`<li>
<a
href="${ifDefined(stage.configureUrl)}${AndNext(
`${globalAK().api.relBase}if/user/#/settings;${JSON.stringify({
page: "page-mfa",
})}`,
)}"
class="pf-c-dropdown__menu-item"
>
<a href=${ifDefined(href)} class="pf-c-dropdown__menu-item">
${stageToAuthenticatorName(stage)}
</a>
</li>`;