mirror of
https://github.com/goauthentik/authentik
synced 2026-05-15 11:26:31 +02:00
Compare commits
1 Commits
sdko/captc
...
basepath-a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0fd4d4a34 |
@@ -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]
|
||||
],
|
||||
]]
|
||||
|
||||
@@ -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" });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:") {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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("-")}`,
|
||||
|
||||
@@ -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>`}`;
|
||||
};
|
||||
|
||||
|
||||
@@ -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")}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>`,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>`,
|
||||
|
||||
75
web/src/admin/navigation.ts
Normal file
75
web/src/admin/navigation.ts
Normal 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}`);
|
||||
@@ -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>`,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>`;
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
@@ -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>`,
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
@@ -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>`,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
@@ -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>`,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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``
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
@@ -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>`,
|
||||
|
||||
@@ -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>`;
|
||||
|
||||
@@ -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>`,
|
||||
|
||||
@@ -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`<${msg("No name set")}>`}</small>
|
||||
</a>`,
|
||||
|
||||
@@ -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}`,
|
||||
);
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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))}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"}>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
|
||||
35
web/src/elements/router/constants.ts
Normal file
35
web/src/elements/router/constants.ts
Normal 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";
|
||||
143
web/src/elements/router/navigation.ts
Normal file
143
web/src/elements/router/navigation.ts
Normal 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 });
|
||||
}
|
||||
140
web/src/elements/router/parsing.ts
Normal file
140
web/src/elements/router/parsing.ts
Normal 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));
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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) ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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>`;
|
||||
}
|
||||
|
||||
@@ -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>`;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
7
web/src/user/navigation.ts
Normal file
7
web/src/user/navigation.ts
Normal 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);
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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>`;
|
||||
|
||||
Reference in New Issue
Block a user