Compare commits

...

1 Commits

Author SHA1 Message Date
e4c8c79ed4 web: Update WebDriver types. Fix issues surrounding async tests.
WIP;

WIP 2

web: Flesh out fixtures, test IDs.

web: Flesh out provider tests.

web: Flesh out LDAP test.

web: Fix typo.

web: Allow base URL to be updated.

web: Clean up.

web: Tidy types.

web: Update ARIA attributes for better test targeting.

web: Clean up message labeling.

web: Clean up ARIA labels.

web: Flesh out table ARIA labels.

web: Flesh out series.

web: Fix linter.

web: Clean up test reporting, timing issues. Add RADIUS test.
2025-06-17 21:16:00 +02:00
234 changed files with 7333 additions and 10350 deletions

View File

@ -141,6 +141,7 @@ skip = [
"**/web/src/locales",
"**/web/xliff",
"**/web/out",
"**/web/playwright-report",
"./web/storybook-static",
"./web/custom-elements.json",
"./website/build",

2
web/.gitignore vendored
View File

@ -25,6 +25,8 @@ lib-cov
# Coverage directory used by tools like istanbul
coverage
playwright-report
test-results
*.lcov
# nyc test coverage

View File

@ -27,11 +27,6 @@ const inlineCSSPlugin = {
},
};
/**
* @satisfies {InlineConfig}
*/
// const viteFinal = ;
/**
* @satisfies {StorybookConfig}
*/

89
web/e2e/elements/proxy.ts Normal file
View File

@ -0,0 +1,89 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { LocatorContext } from "#e2e/selectors/types";
import { ConsoleLogger } from "#logger/node";
import { Locator, Page, expect } from "@playwright/test";
import { kebabCase } from "change-case";
export type LocatorMatchers = ReturnType<typeof expect<Locator>>;
export interface LocatorProxy extends Pick<Locator, keyof Locator> {
$: Locator;
expect: LocatorMatchers;
}
// Type helpers to extract the shape of the proxy
export type DeepLocatorProxy<T> =
Disposable & T extends Record<string, any>
? T extends HTMLElement
? LocatorProxy
: {
[K in keyof T]: DeepLocatorProxy<T[K]>;
}
: LocatorProxy;
export function createLocatorProxy<T extends Record<string, any>>(
ctx: LocatorContext,
initialPathPrefix: string[] = [],
dataAttribute: string = "test-id",
): DeepLocatorProxy<T> {
dataAttribute = kebabCase(dataAttribute);
function createProxy(path: string[] = initialPathPrefix): any {
const proxyCache = new Map<string, LocatorProxy>();
return new Proxy({} as any, {
get(_, property: string) {
// Build the current path
const currentPath = [...path, property];
// Convert the path to kebab-case and join with hyphens
const selectorValue = currentPath.map((segment) => kebabCase(segment)).join("-");
const selector = `[data-${dataAttribute}="${selectorValue}"]`;
// Create a locator for the current selector
const locator = ctx.locator(selector);
if (proxyCache.has(selector)) {
ConsoleLogger.debug(`Using cached locator for ${selector}`);
return proxyCache.get(selector)!;
}
// Return a new proxy that also behaves like a Locator
// This allows us to either continue chaining or use Locator methods
const nextProxy = new Proxy(locator, {
get(target, prop) {
if (typeof prop === "string") {
// The user is likely trying to access a property on the page.
if (prop === "$") {
return target as any;
}
if (prop === "expect") {
return expect(target);
}
}
// If the property exists on the Locator, use it
if (prop in target) {
const value = (target as any)[prop];
// Bind methods to the locator instance
if (typeof value === "function") {
return value.bind(target);
}
return value;
}
// Otherwise, continue building the path
return createProxy(currentPath)[prop];
},
});
proxyCache.set(selector, nextProxy as LocatorProxy);
return nextProxy;
},
});
}
return createProxy() as DeepLocatorProxy<T>;
}

View File

@ -0,0 +1,174 @@
import { PageFixture } from "#e2e/fixtures/PageFixture";
import type { LocatorContext } from "#e2e/selectors/types";
import { Page, expect } from "@playwright/test";
export class FormFixture extends PageFixture {
static fixtureName = "Form";
//#region Selector Methods
//#endregion
//#region Field Methods
/**
* Set the value of a text input.
*
* @param fieldName The name of the form element.
* @param value the value to set.
*/
public fill = async (
fieldName: string,
value: string,
parent: LocatorContext = this.page,
): Promise<void> => {
const control = parent
.getByRole("textbox", {
name: fieldName,
})
.or(
parent.getByRole("spinbutton", {
name: fieldName,
}),
)
.first();
await expect(control, `Field (${fieldName}) should be visible`).toBeVisible();
await control.fill(value);
};
/**
* Set the value of a radio or checkbox input.
*
* @param fieldName The name of the form element.
* @param value the value to set.
*/
public setInputCheck = async (
fieldName: string,
value: boolean = true,
parent: LocatorContext = this.page,
): Promise<void> => {
const control = parent.locator("ak-switch-input", {
hasText: fieldName,
});
await control.scrollIntoViewIfNeeded();
await expect(control, `Field (${fieldName}) should be visible`).toBeVisible();
const currentChecked = await control
.getAttribute("checked")
.then((value) => value !== null);
if (currentChecked === value) {
return;
}
await control.click();
};
/**
* Set the value of a radio or checkbox input.
*
* @param fieldName The name of the form element.
* @param pattern the value to set.
*/
public setRadio = async (
groupName: string,
fieldName: string,
parent: LocatorContext = this.page,
): Promise<void> => {
const group = parent.getByRole("group", { name: groupName });
await expect(group, `Field "${groupName}" should be visible`).toBeVisible();
const control = parent.getByRole("radio", { name: fieldName });
await control.setChecked(true, {
force: true,
});
};
/**
* Set the value of a search select input.
*
* @param fieldLabel The name of the search select element.
* @param pattern The text to match against the search select entry.
*/
public selectSearchValue = async (
fieldLabel: string,
pattern: string | RegExp,
parent: LocatorContext = this.page,
): Promise<void> => {
const control = parent.getByRole("textbox", { name: fieldLabel });
await expect(
control,
`Search select control (${fieldLabel}) should be visible`,
).toBeVisible();
const fieldName = await control.getAttribute("name");
if (!fieldName) {
throw new Error(`Unable to find name attribute on search select (${fieldLabel})`);
}
// Find the search select input control and activate it.
await control.click();
const button = this.page
// ---
.locator(`div[data-managed-for*="${fieldName}"] button`, {
hasText: pattern,
});
if (!button) {
throw new Error(
`Unable to find an ak-search-select entry matching ${fieldLabel}:${pattern.toString()}`,
);
}
await button.click();
await this.page.keyboard.press("Tab");
await control.blur();
};
public setFormGroup = async (
pattern: string | RegExp,
value: boolean = true,
parent: LocatorContext = this.page,
) => {
const control = parent
.locator("ak-form-group", {
hasText: pattern,
})
.first();
const currentOpen = await control.getAttribute("open").then((value) => value !== null);
if (currentOpen === value) {
this.logger.debug(`Form group ${pattern} is already ${value ? "open" : "closed"}`);
return;
}
this.logger.debug(`Toggling form group ${pattern} to ${value ? "open" : "closed"}`);
await control.click();
if (value) {
await expect(control).toHaveAttribute("open");
} else {
await expect(control).not.toHaveAttribute("open");
}
};
//#endregion
//#region Lifecycle
constructor(page: Page, testName: string) {
super({ page, testName });
}
//#endregion
}

View File

@ -0,0 +1,29 @@
import { ConsoleLogger, FixtureLogger } from "#logger/node";
import { Page } from "@playwright/test";
export interface PageFixtureOptions {
page: Page;
testName: string;
}
export abstract class PageFixture {
/**
* The name of the fixture.
*
* Used for logging.
*/
static fixtureName: string;
protected readonly logger: FixtureLogger;
protected readonly page: Page;
protected readonly testName: string;
constructor({ page, testName }: PageFixtureOptions) {
this.page = page;
this.testName = testName;
const Constructor = this.constructor as typeof PageFixture;
this.logger = ConsoleLogger.fixture(Constructor.fixtureName, this.testName);
}
}

View File

@ -0,0 +1,41 @@
import { PageFixture } from "#e2e/fixtures/PageFixture";
import type { LocatorContext } from "#e2e/selectors/types";
import { Page } from "@playwright/test";
export type GetByRoleParameters = Parameters<Page["getByRole"]>;
export type ARIARole = GetByRoleParameters[0];
export type ARIAOptions = GetByRoleParameters[1];
export type ClickByName = (name: string) => Promise<void>;
export type ClickByRole = (
role: ARIARole,
options?: ARIAOptions,
context?: LocatorContext,
) => Promise<void>;
export class PointerFixture extends PageFixture {
public static fixtureName = "Pointer";
public click = (
name: string,
optionsOrRole?: ARIAOptions | ARIARole,
context: LocatorContext = this.page,
): Promise<void> => {
if (typeof optionsOrRole === "string") {
return context.getByRole(optionsOrRole, { name }).click();
}
const options = {
...optionsOrRole,
name,
};
return (
context
// ---
.getByRole("button", options)
.or(context.getByRole("link", options))
.click()
);
};
}

View File

@ -0,0 +1,118 @@
import { PageFixture } from "#e2e/fixtures/PageFixture";
import { Page, expect } from "@playwright/test";
export const GOOD_USERNAME = "test-admin@goauthentik.io";
export const GOOD_PASSWORD = "test-runner";
export const BAD_USERNAME = "bad-username@bad-login.io";
export const BAD_PASSWORD = "-this-is-a-bad-password-";
export interface LoginInit {
username?: string;
password?: string;
to?: URL | string;
}
export class SessionFixture extends PageFixture {
static fixtureName = "Session";
public static readonly pathname = "/if/flow/default-authentication-flow/";
//#region Selectors
public $identificationStage = this.page.locator("ak-stage-identification");
/**
* The username field on the login page.
*/
public $usernameField = this.$identificationStage.locator('input[name="uidField"]');
/**
* The button to continue with the login process,
* typically to the password flow stage.
*/
public $submitUsernameStageButton = this.$identificationStage.locator('button[type="submit"]');
public $passwordStage = this.page.locator("ak-stage-password");
public $passwordField = this.$passwordStage.locator('input[name="password"]');
/**
* The button to submit the the login flow,
* typically redirecting to the authenticated interface.
*/
public $submitPasswordStageButton = this.$passwordStage.locator('button[type="submit"]');
/**
* A possible authentication failure message.
*/
public $authFailureMessage = this.page.locator(".pf-m-error");
//#endregion
constructor(page: Page, testName: string) {
super({ page, testName });
}
//#region Specific interactions
public async submitUsernameStage(username: string) {
this.logger.info("Submitting username stage", username);
await this.$usernameField.fill(username);
await expect(this.$submitUsernameStageButton).toBeEnabled();
await this.$submitUsernameStageButton.click();
}
public async submitPasswordStage(password: string) {
this.logger.info("Submitting password stage");
await this.$passwordField.fill(password);
await expect(this.$submitPasswordStageButton).toBeEnabled();
await this.$submitPasswordStageButton.click();
}
public checkAuthenticated = async (): Promise<boolean> => {
// TODO: Check if the user is authenticated via API
return true;
};
/**
* Log into the application.
*/
public async login({
username = GOOD_USERNAME,
password = GOOD_PASSWORD,
to = SessionFixture.pathname,
}: LoginInit = {}) {
this.logger.info("Logging in...");
const initialURL = new URL(this.page.url());
if (initialURL.pathname === SessionFixture.pathname) {
this.logger.info("Skipping navigation because we're already in a authentication flow");
} else {
await this.page.goto(to.toString());
}
await this.submitUsernameStage(username);
await this.$passwordField.waitFor({ state: "visible" });
await this.submitPasswordStage(password);
const expectedPathname = typeof to === "string" ? to : to.pathname;
await this.page.waitForURL(`**${expectedPathname}`);
}
//#endregion
//#region Navigation
public async toLoginPage() {
await this.page.goto(SessionFixture.pathname);
}
}

55
web/e2e/index.ts Normal file
View File

@ -0,0 +1,55 @@
/* eslint-disable react-hooks/rules-of-hooks */
import { DeepLocatorProxy, createLocatorProxy } from "#e2e/elements/proxy";
import { FormFixture } from "#e2e/fixtures/FormFixture";
import { PointerFixture } from "#e2e/fixtures/PointerFixture";
import { SessionFixture } from "#e2e/fixtures/SessionFixture";
import { createOUIDNameEngine } from "#e2e/selectors/ouid";
import { type Page, test as base } from "@playwright/test";
export { expect } from "@playwright/test";
type TestIDLocatorProxy = DeepLocatorProxy<TestIDSelectorMap>;
interface E2EFixturesTestScope {
/**
* A proxy to retrieve elements by test ID.
*
* ```ts
* const $button = $.button;
* ```
*/
$: TestIDLocatorProxy;
session: SessionFixture;
pointer: PointerFixture;
form: FormFixture;
}
interface E2EWorkerScope {
selectorRegistration: void;
}
export const test = base.extend<E2EFixturesTestScope, E2EWorkerScope>({
selectorRegistration: [
async ({ playwright }, use) => {
await playwright.selectors.register("ouid", createOUIDNameEngine);
await use();
},
{ auto: true, scope: "worker" },
],
$: async ({ page }, use) => {
await use(createLocatorProxy<TestIDSelectorMap>(page));
},
session: async ({ page }, use, { title }) => {
await use(new SessionFixture(page, title));
},
form: async ({ page }, use, { title }) => {
await use(new FormFixture(page, title));
},
pointer: async ({ page }, use, { title }) => {
await use(new PointerFixture({ page, testName: title }));
},
});

44
web/e2e/selectors/ouid.ts Normal file
View File

@ -0,0 +1,44 @@
/* eslint-disable no-console */
type SelectorRoot = Document | ShadowRoot;
export function createOUIDNameEngine() {
const attributeName = "data-ouid-component-name";
console.log("Creating OUID selector engine!!");
return {
// Returns all elements matching given selector in the root's subtree.
queryAll(scope: SelectorRoot, componentName: string) {
const result: Element[] = [];
const match = (element: Element) => {
const name = element.getAttribute(attributeName);
if (name === componentName) {
result.push(element);
}
};
const query = (root: Element | ShadowRoot | Document) => {
const shadows: ShadowRoot[] = [];
if ((root as Element).shadowRoot) {
shadows.push((root as Element).shadowRoot!);
}
for (const element of root.querySelectorAll("*")) {
match(element);
if (element.shadowRoot) {
shadows.push(element.shadowRoot);
}
}
shadows.forEach(query);
};
query(scope);
return result;
},
};
}

View File

@ -0,0 +1,13 @@
import type { Locator } from "@playwright/test";
export type LocatorContext = Pick<
Locator,
| "locator"
| "getByRole"
| "getByTestId"
| "getByText"
| "getByLabel"
| "getByAltText"
| "getByTitle"
| "getByPlaceholder"
>;

View File

@ -0,0 +1,59 @@
import { IDGenerator } from "@goauthentik/core/id";
import {
Config as NameConfig,
adjectives,
colors,
uniqueNamesGenerator,
} from "unique-names-generator";
/**
* Given a dictionary of words, slice the dictionary to only include words that start with the given letter.
*/
export function alliterate(dictionary: string[], letter: string): string[] {
let firstIndex = 0;
for (let i = 0; i < dictionary.length; i++) {
if (dictionary[i][0] === letter) {
firstIndex = i;
break;
}
}
let lastIndex = firstIndex;
for (let i = firstIndex; i < dictionary.length; i++) {
if (dictionary[i][0] !== letter) {
lastIndex = i;
break;
}
}
return dictionary.slice(firstIndex, lastIndex);
}
export function createRandomName({
seed = IDGenerator.randomID(),
...config
}: Partial<NameConfig> = {}) {
const randomLetterIndex =
typeof seed === "number"
? seed
: Array.from(seed).reduce((acc, char) => acc + char.charCodeAt(0), 0);
const letter = adjectives[randomLetterIndex % adjectives.length][0];
const availableAdjectives = alliterate(adjectives, letter);
const availableColors = alliterate(colors, letter);
const name = uniqueNamesGenerator({
dictionaries: [availableAdjectives, availableAdjectives, availableColors],
style: "capital",
separator: " ",
length: 3,
seed,
...config,
});
return name;
}

101
web/logger/node.js Normal file
View File

@ -0,0 +1,101 @@
/**
* Application logger.
*
* @import { LoggerOptions, Logger, Level, ChildLoggerOptions } from "pino"
* @import { PrettyOptions } from "pino-pretty"
*/
import { pino } from "pino";
//#region Constants
/**
* Default options for creating a Pino logger.
*
* @category Logger
* @satisfies {LoggerOptions<never, false>}
*/
export const DEFAULT_PINO_LOGGER_OPTIONS = {
enabled: true,
level: "info",
transport: {
target: "./transport.js",
options: /** @satisfies {PrettyOptions} */ ({
colorize: true,
}),
},
};
//#endregion
//#region Functions
/**
* Read the log level from the environment.
* @return {Level}
*/
export function readLogLevel() {
return process.env.AK_LOG_LEVEL || DEFAULT_PINO_LOGGER_OPTIONS.level;
}
/**
* @typedef {Logger} FixtureLogger
*/
/**
* @this {Logger}
* @param {string} fixtureName
* @param {string} [testName]
* @param {ChildLoggerOptions} [options]
* @returns {FixtureLogger}
*/
function createFixtureLogger(fixtureName, testName, options) {
return this.child(
{ name: fixtureName },
{
msgPrefix: `[${testName}] `,
...options,
},
);
}
/**
* @typedef {object} CustomLoggerMethods
* @property {typeof createFixtureLogger} fixture
*/
/**
* @typedef {Logger & CustomLoggerMethods} ConsoleLogger
*/
/**
* A singleton logger instance for Node.js.
*
* ```js
* import { ConsoleLogger } from "#logger/node";
*
* ConsoleLogger.info("Hello, world!");
* ```
*
* @runtime node
* @type {ConsoleLogger}
*/
export const ConsoleLogger = Object.assign(
pino({
...DEFAULT_PINO_LOGGER_OPTIONS,
level: readLogLevel(),
}),
{ fixture: createFixtureLogger },
);
/**
* @typedef {ReturnType<ConsoleLogger['child']>} ChildConsoleLogger
*/
//#region Aliases
export const info = ConsoleLogger.info.bind(ConsoleLogger);
export const debug = ConsoleLogger.debug.bind(ConsoleLogger);
export const warn = ConsoleLogger.warn.bind(ConsoleLogger);
export const error = ConsoleLogger.error.bind(ConsoleLogger);
//#endregion

21
web/logger/transport.js Normal file
View File

@ -0,0 +1,21 @@
/**
* @file Pretty transport for Pino
*
* @import { PrettyOptions } from "pino-pretty"
*/
import PinoPretty from "pino-pretty";
/**
* @param {PrettyOptions} options
*/
function prettyTransporter(options) {
const pretty = PinoPretty({
...options,
ignore: "pid,hostname",
translateTime: "SYS:HH:MM:ss",
});
return pretty;
}
export default prettyTransporter;

8555
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -24,8 +24,8 @@
"pseudolocalize": "node ./scripts/pseudolocalize.mjs",
"storybook": "storybook dev -p 6006",
"storybook:build": "wireit",
"test": "wireit",
"test:e2e": "wireit",
"test": "vitest",
"test:e2e": "playwright test",
"test:e2e:watch": "wireit",
"test:watch": "wireit",
"tsc": "wireit",
@ -69,6 +69,9 @@
"#flow/*": "./src/flow/*.js",
"#locales/*": "./src/locales/*.js",
"#stories/*": "./src/stories/*.js",
"#tests/*": "./tests/*.js",
"#e2e": "./e2e/index.ts",
"#e2e/*": "./e2e/*.ts",
"#*/browser": {
"types": "./out/*/browser.d.ts",
"import": "./*/browser.js"
@ -93,7 +96,7 @@
"@floating-ui/dom": "^1.6.11",
"@formatjs/intl-listformat": "^7.7.11",
"@fortawesome/fontawesome-free": "^6.7.2",
"@goauthentik/api": "^2025.6.2-1750112513",
"@goauthentik/api": "^2025.6.1-1749515784",
"@lit/context": "^1.1.2",
"@lit/localize": "^0.12.2",
"@lit/reactive-element": "^2.0.4",
@ -102,9 +105,10 @@
"@open-wc/lit-helpers": "^0.7.0",
"@patternfly/elements": "^4.1.0",
"@patternfly/patternfly": "^4.224.2",
"@sentry/browser": "^9.29.0",
"@spotlightjs/spotlight": "^3.0.0",
"@sentry/browser": "^9.28.0",
"@spotlightjs/spotlight": "^2.13.3",
"@webcomponents/webcomponentsjs": "^2.8.0",
"base64-js": "^1.5.1",
"change-case": "^5.4.4",
"chart.js": "^4.4.9",
"chartjs-adapter-date-fns": "^3.0.0",
@ -120,7 +124,9 @@
"hastscript": "^9.0.1",
"lit": "^3.2.0",
"md-front-matter": "^1.0.4",
"mermaid": "^11.6.0",
"mermaid": "^11.4.1",
"pino": "^9.7.0",
"pino-pretty": "^13.0.0",
"rapidoc": "^9.3.8",
"react": "^19.1.0",
"react-dom": "^19.1.0",
@ -136,7 +142,6 @@
"trusted-types": "^2.0.0",
"ts-pattern": "^5.7.1",
"unist-util-visit": "^5.0.0",
"webauthn-polyfills": "^0.1.7",
"webcomponent-qr-code": "^1.2.0",
"yaml": "^2.8.0"
},
@ -149,6 +154,7 @@
"@goauthentik/tsconfig": "^1.0.4",
"@hcaptcha/types": "^1.0.4",
"@lit/localize-tools": "^0.8.0",
"@playwright/test": "^1.52.0",
"@storybook/addon-essentials": "^8.6.14",
"@storybook/addon-links": "^8.6.14",
"@storybook/blocks": "^8.6.12",
@ -158,6 +164,7 @@
"@storybook/test": "^8.6.14",
"@storybook/web-components": "^8.6.14",
"@storybook/web-components-vite": "^8.6.14",
"@testing-library/dom": "^10.4.0",
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
"@types/chart.js": "^2.9.41",
"@types/codemirror": "^5.60.15",
@ -170,16 +177,14 @@
"@types/react-dom": "^19.1.5",
"@typescript-eslint/eslint-plugin": "^8.8.0",
"@typescript-eslint/parser": "^8.8.0",
"@wdio/browser-runner": "9.15",
"@wdio/cli": "9.15",
"@vitest/browser": "^3.2.0",
"@wdio/cli": "^9.15.0",
"@wdio/spec-reporter": "^9.15.0",
"@web/test-runner": "^0.20.2",
"chromedriver": "^136.0.3",
"esbuild": "^0.25.5",
"esbuild": "^0.25.4",
"esbuild-plugin-copy": "^2.1.1",
"esbuild-plugin-polyfill-node": "^0.3.0",
"esbuild-plugins-node-modules-polyfill": "^1.7.0",
"eslint": "^9.29.0",
"eslint": "^9.28.0",
"eslint-plugin-lit": "^2.1.1",
"eslint-plugin-wc": "^3.0.1",
"github-slugger": "^2.0.0",
@ -187,16 +192,22 @@
"knip": "^5.58.0",
"lit-analyzer": "^2.0.3",
"npm-run-all": "^4.1.5",
"p-iteration": "^1.1.8",
"playwright": "^1.52.0",
"prettier": "^3.3.3",
"pseudolocale": "^2.1.0",
"rollup-plugin-postcss-lit": "^2.2.0",
"storybook": "^8.6.14",
"storybook-addon-mock": "^5.0.0",
"turnstile-types": "^1.2.3",
"type-fest": "^4.41.0",
"typescript": "^5.8.3",
"typescript-eslint": "^8.34.1",
"vite-plugin-lit-css": "^2.0.0",
"typescript-eslint": "^8.34.0",
"unique-names-generator": "^4.7.1",
"vite": "^6.3.5",
"vite-plugin-lit-css": "^2.1.0",
"vite-tsconfig-paths": "^5.0.1",
"vitest": "^3.2.0",
"wireit": "^0.14.12"
},
"optionalDependencies": {
@ -278,7 +289,7 @@
"command": "lit-analyzer src"
},
"lint:types:tests": {
"command": "tsc --noEmit -p ./tests"
"command": "tsc --noEmit -p tsconfig.test.json"
},
"lint:types": {
"command": "tsc -p .",
@ -314,33 +325,33 @@
}
},
"test": {
"command": "wdio ./wdio.conf.ts --logLevel=warn",
"command": "wdio ./wdio.conf.js --logLevel=warn",
"env": {
"CI": "true",
"TS_NODE_PROJECT": "tsconfig.test.json"
}
},
"test:e2e": {
"command": "wdio run ./tests/wdio.conf.ts",
"command": "wdio run ./tests/wdio.conf.js",
"dependencies": [
"build"
],
"env": {
"CI": "true",
"TS_NODE_PROJECT": "./tests/tsconfig.test.json"
"TS_NODE_PROJECT": "tsconfig.test.json"
}
},
"test:e2e:watch": {
"command": "wdio run ./tests/wdio.conf.ts",
"command": "wdio run ./tests/wdio.conf.js",
"dependencies": [
"build"
],
"env": {
"TS_NODE_PROJECT": "./tests/tsconfig.test.json"
"TS_NODE_PROJECT": "tsconfig.test.json"
}
},
"test:watch": {
"command": "wdio run ./wdio.conf.ts",
"command": "wdio run ./wdio.conf.js",
"dependencies": [
"build"
],

View File

@ -0,0 +1,50 @@
/**
* @file Unique ID utilities.
*/
/**
* A global ID generator.
*
* @singleton
* @runtime common
*
* @category IDs
*/
export class IDGenerator {
static #sequenceIndex = 0;
static #elementIndex = 0;
/**
* Create a new ID for an HTML element.
*
* This ID will be unique for the lifetime of the page and will not be
* exposed on the `window` object.
*
* @param {string | number} [name] An optional name to use for the element.
*/
static elementID(name) {
name = name || ++this.#elementIndex;
return "«ak-" + name + "»";
}
/**
* Create a new ID.
*/
static next() {
this.#sequenceIndex += 1;
return this.#sequenceIndex;
}
/**
* Generate a random ID in hexadecimal format.
*
* @param {number} [characterLength]
*/
static randomID(characterLength = 6) {
const bytes = crypto.getRandomValues(new Uint8Array(characterLength / 2));
return Array.from(bytes, (a) => a.toString(16)).join("");
}
}

View File

@ -0,0 +1,27 @@
/**
* @file Helpers for running tests.
*/
/**
* A function that returns a promise.
* @template {never[]} [A=never[]]
* @typedef {(...args: A) => Promise<unknown>} Thenable
*/
/**
* A tuple of a function and its arguments.
* @template {Thenable} [T=Thenable]
* @typedef {[T, Parameters<T>]} SerializedThenable
*/
/**
* Executes a sequence of promise-returning functions in series
* @template {Thenable[]} T
* @param {{ [K in keyof T]: [T[K], ...Parameters<T[K]>] }} sequence
* @returns {Promise<void>}
*/
export async function series(...sequence) {
for (const [thenable, ...args] of sequence) {
await thenable(...args);
}
}

View File

@ -14,7 +14,7 @@ declare module "module" {
* const relativeDirname = dirname(fileURLToPath(import.meta.url));
* ```
*/
// eslint-disable-next-line no-var
var __dirname: string;
}
}

92
web/playwright.config.js Normal file
View File

@ -0,0 +1,92 @@
/**
* @file Playwright configuration.
*
* @see https://playwright.dev/docs/test-configuration
*
* @import { LogFn, Logger } from "pino"
*/
import { ConsoleLogger } from "#logger/node";
import { defineConfig, devices } from "@playwright/test";
const CI = !!process.env.CI;
/**
* @type {Map<string, Logger>}
*/
const LoggerCache = new Map();
const baseURL = process.env.AK_TEST_RUNNER_PAGE_URL ?? "http://localhost:9000";
export default defineConfig({
testDir: "./test/browser",
fullyParallel: true,
forbidOnly: CI,
retries: CI ? 2 : 0,
workers: CI ? 1 : undefined,
reporter: CI
? "github"
: [
// ---
["list", { printSteps: true }],
["html", { open: "never" }],
],
use: {
testIdAttribute: "data-test-id",
baseURL,
trace: "on-first-retry",
launchOptions: {
logger: {
isEnabled() {
return true;
},
log: (name, severity, message, args) => {
let logger = LoggerCache.get(name);
if (!logger) {
logger = ConsoleLogger.child({
name: `Playwright ${name.toUpperCase()}`,
});
LoggerCache.set(name, logger);
}
/**
* @type {LogFn}
*/
let log;
switch (severity) {
case "verbose":
log = logger.debug;
break;
case "warning":
log = logger.warn;
break;
case "error":
log = logger.error;
break;
default:
log = logger.info;
break;
}
if (name === "api") {
log = logger.debug;
}
log.call(logger, message.toString(), args);
},
},
},
},
/* Configure projects for major browsers */
projects: [
{
name: "chromium",
use: {
...devices["Desktop Chrome"],
},
},
],
});

View File

@ -6,8 +6,9 @@ import "@goauthentik/elements/EmptyState";
import { ModalButton } from "@goauthentik/elements/buttons/ModalButton";
import { msg } from "@lit/localize";
import { TemplateResult, css, html } from "lit";
import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement } from "lit/decorators.js";
import { createRef, ref } from "lit/directives/ref.js";
import { until } from "lit/directives/until.js";
import PFAbout from "@patternfly/patternfly/components/AboutModalBox/about-modal-box.css";
@ -16,16 +17,15 @@ import { AdminApi, CapabilitiesEnum, LicenseSummaryStatusEnum } from "@goauthent
@customElement("ak-about-modal")
export class AboutModal extends WithLicenseSummary(WithBrandConfig(ModalButton)) {
static get styles() {
return ModalButton.styles.concat(
PFAbout,
css`
.pf-c-about-modal-box__hero {
background-image: url("/static/dist/assets/images/flow_background.jpg");
}
`,
);
}
static styles: CSSResult[] = [
...ModalButton.styles,
PFAbout,
css`
.pf-c-about-modal-box__hero {
background-image: url("/static/dist/assets/images/flow_background.jpg");
}
`,
];
async getAboutEntries(): Promise<[string, string | TemplateResult][]> {
const status = await new AdminApi(DEFAULT_CONFIG).adminSystemRetrieve();
@ -55,21 +55,32 @@ export class AboutModal extends WithLicenseSummary(WithBrandConfig(ModalButton))
];
}
renderModal() {
#contentRef = createRef<HTMLDivElement>();
#backdropListener = (event: PointerEvent) => {
// We only want to close the modal when the backdrop is clicked, not when it's children are clicked.
if (this.#contentRef.value?.contains(event.target as Node)) {
return;
}
this.close();
};
protected override renderModal() {
let product = this.brandingTitle;
if (this.licenseSummary.status !== LicenseSummaryStatusEnum.Unlicensed) {
product += ` ${msg("Enterprise")}`;
}
return html`<div
class="pf-c-backdrop"
@click=${(e: PointerEvent) => {
e.stopPropagation();
this.closeModal();
}}
>
return html`<div class="pf-c-backdrop" @click=${this.#backdropListener}>
<div class="pf-l-bullseye">
<div class="pf-c-about-modal-box" role="dialog" aria-modal="true">
<div
${ref(this.#contentRef)}
class="pf-c-about-modal-box"
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
>
<div class="pf-c-about-modal-box__brand">
<img
class="pf-c-about-modal-box__brand-image"
@ -78,18 +89,12 @@ export class AboutModal extends WithLicenseSummary(WithBrandConfig(ModalButton))
/>
</div>
<div class="pf-c-about-modal-box__close">
<button
class="pf-c-button pf-m-plain"
type="button"
@click=${() => {
this.open = false;
}}
>
<button class="pf-c-button pf-m-plain" type="button" @click=${this.close}>
<i class="fas fa-times" aria-hidden="true"></i>
</button>
</div>
<div class="pf-c-about-modal-box__header">
<h1 class="pf-c-title pf-m-4xl">${product}</h1>
<h1 class="pf-c-title pf-m-4xl" id="modal-title">${product}</h1>
</div>
<div class="pf-c-about-modal-box__hero"></div>
<div class="pf-c-about-modal-box__content">

View File

@ -1,16 +1,44 @@
import { SidebarItemProperties } from "#elements/sidebar/SidebarItem";
import { ID_REGEX, SLUG_REGEX, UUID_REGEX } from "@goauthentik/elements/router/Route";
import { spread } from "@open-wc/lit-helpers";
import { msg } from "@lit/localize";
import { TemplateResult, html, nothing } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
import { repeat } from "lit/directives/repeat.js";
/**
* Given a record-like object, prefixes each key with a dot, allowing it to be spread into a
* template literal.
*
* ```ts
* interface MyElementProperties {
* foo: string;
* bar: number;
* }
*
* const properties {} as LitPropertyRecord<MyElementProperties>
*
* console.log(properties) // { '.foo': string; '.bar': number }
* ```
*/
export type LitPropertyRecord<T extends object> = {
[K in keyof T as K extends string ? LitPropertyKey<K> : never]: T[K];
};
/**
* A type that represents a property key that can be used in a LitPropertyRecord.
*
* @see {@linkcode LitPropertyRecord}
*/
export type LitPropertyKey<K> = K extends string ? `.${K}` | `?${K}` | K : K;
// The second attribute type is of string[] to help with the 'activeWhen' control, which was
// commonplace and singular enough to merit its own handler.
type SidebarEntry = [
path: string | null,
label: string,
attributes?: Record<string, any> | string[] | null, // eslint-disable-line
attributes?: LitPropertyRecord<SidebarItemProperties> | string[] | null,
children?: SidebarEntry[],
];
@ -31,8 +59,7 @@ export function renderSidebarItem([
properties.path = path;
}
return html`<ak-sidebar-item ${spread(properties)}>
${label ? html`<span slot="label">${label}</span>` : nothing}
return html`<ak-sidebar-item label=${ifDefined(label)} ${spread(properties)}>
${children ? renderSidebarItems(children) : nothing}
</ak-sidebar-item>`;
}

View File

@ -7,6 +7,7 @@ import { me } from "#common/users";
import { WebsocketClient } from "#common/ws";
import { SidebarToggleEventDetail } from "#components/ak-page-header";
import { AuthenticatedInterface } from "#elements/AuthenticatedInterface";
import "#elements/a11y/ak-skip-to-content";
import "#elements/ak-locale-context/ak-locale-context";
import "#elements/banner/EnterpriseStatusBanner";
import "#elements/banner/EnterpriseStatusBanner";
@ -22,6 +23,7 @@ import "#elements/router/RouterOutlet";
import "#elements/sidebar/Sidebar";
import "#elements/sidebar/SidebarItem";
import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, css, html, nothing } from "lit";
import { customElement, eventOptions, property, query } from "lit/decorators.js";
import { classMap } from "lit/directives/class-map.js";
@ -162,16 +164,18 @@ export class AdminInterface extends WithCapabilitiesConfig(AuthenticatedInterfac
}
async firstUpdated(): Promise<void> {
this.user = await me();
me().then((session) => {
this.user = session;
const canAccessAdmin =
this.user.user.isSuperuser ||
// TODO: somehow add `access_admin_interface` to the API schema
this.user.user.systemPermissions.includes("access_admin_interface");
const canAccessAdmin =
this.user.user.isSuperuser ||
// TODO: somehow add `access_admin_interface` to the API schema
this.user.user.systemPermissions.includes("access_admin_interface");
if (!canAccessAdmin && this.user.user.pk > 0) {
window.location.assign("/if/user/");
}
if (!canAccessAdmin && this.user.user.pk > 0) {
window.location.assign("/if/user/");
}
});
}
render(): TemplateResult {
@ -190,13 +194,14 @@ export class AdminInterface extends WithCapabilitiesConfig(AuthenticatedInterfac
};
return html` <ak-locale-context>
<ak-skip-to-content></ak-skip-to-content>
<div class="pf-c-page">
<ak-page-navbar ?open=${this.sidebarOpen} @sidebar-toggle=${this.sidebarListener}>
<ak-version-banner></ak-version-banner>
<ak-enterprise-status interface="admin"></ak-enterprise-status>
</ak-page-navbar>
<ak-sidebar class="${classMap(sidebarClasses)}">
<ak-sidebar ?hidden=${!this.sidebarOpen} class="${classMap(sidebarClasses)}">
${renderSidebarItems(AdminSidebarEntries)}
${this.can(CapabilitiesEnum.IsEnterprise)
? renderSidebarItems(AdminSidebarEnterpriseEntries)
@ -208,9 +213,10 @@ export class AdminInterface extends WithCapabilitiesConfig(AuthenticatedInterfac
<div class="pf-c-drawer__main">
<div class="pf-c-drawer__content">
<div class="pf-c-drawer__body">
<main class="pf-c-page__main">
<div class="pf-c-page__main">
<ak-router-outlet
role="main"
aria-label="${msg("Main content")}"
class="pf-c-page__main"
tabindex="-1"
id="main-content"
@ -218,7 +224,7 @@ export class AdminInterface extends WithCapabilitiesConfig(AuthenticatedInterfac
.routes=${ROUTES}
>
</ak-router-outlet>
</main>
</div>
</div>
</div>
<ak-notification-drawer

View File

@ -1,3 +1,4 @@
import { SlottedTemplateResult } from "#elements/types";
import { EVENT_REFRESH } from "@goauthentik/common/constants";
import { PFSize } from "@goauthentik/common/enums.js";
import {
@ -13,7 +14,7 @@ import { state } from "lit/decorators.js";
export interface AdminStatus {
icon: string;
message?: TemplateResult;
message?: SlottedTemplateResult;
}
/**
@ -98,8 +99,8 @@ export abstract class AdminStatusCard<T> extends AggregateCard {
*
* @returns TemplateResult displaying the value
*/
protected renderValue(): TemplateResult {
return html`${this.value}`;
protected renderValue(): SlottedTemplateResult {
return this.value ? html`${this.value}` : nothing;
}
/**
@ -108,7 +109,7 @@ export abstract class AdminStatusCard<T> extends AggregateCard {
* @param status - AdminStatus object containing icon and message
* @returns TemplateResult for status display
*/
private renderStatus(status: AdminStatus): TemplateResult {
private renderStatus(status: AdminStatus): SlottedTemplateResult {
return html`
<p><i class="${status.icon}"></i>&nbsp;${this.renderValue()}</p>
${status.message ? html`<p class="subtext">${status.message}</p>` : nothing}
@ -121,9 +122,9 @@ export abstract class AdminStatusCard<T> extends AggregateCard {
* @param error - Error message to display
* @returns TemplateResult for error display
*/
private renderError(error: string): TemplateResult {
private renderError(error: string): SlottedTemplateResult {
return html`
<p><i class="fa fa-times"></i>&nbsp;${msg("Failed to fetch")}</p>
<p><i aria-hidden="true" class="fa fa-times"></i>&nbsp;${msg("Failed to fetch")}</p>
<p class="subtext">${error}</p>
`;
}
@ -133,7 +134,7 @@ export abstract class AdminStatusCard<T> extends AggregateCard {
*
* @returns TemplateResult for loading spinner
*/
private renderLoading(): TemplateResult {
private renderLoading(): SlottedTemplateResult {
return html`<ak-spinner size="${PFSize.Large}"></ak-spinner>`;
}
@ -142,7 +143,7 @@ export abstract class AdminStatusCard<T> extends AggregateCard {
*
* @returns TemplateResult for current component state
*/
renderInner(): TemplateResult {
renderInner(): SlottedTemplateResult {
return html`
<p class="center-value">
${

View File

@ -1,3 +1,4 @@
import { SlottedTemplateResult } from "#elements/types";
import {
AdminStatus,
AdminStatusCard,
@ -5,7 +6,7 @@ import {
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit";
import { TemplateResult, html, nothing } from "lit";
import { customElement, state } from "lit/decorators.js";
import { AdminApi, OutpostsApi, SystemInfo } from "@goauthentik/api";
@ -84,12 +85,12 @@ export class SystemStatusCard extends AdminStatusCard<SystemInfo> {
});
}
renderHeader(): TemplateResult {
return html`${msg("System status")}`;
renderHeader(): SlottedTemplateResult {
return msg("System status");
}
renderValue(): TemplateResult {
return html`${this.statusSummary}`;
renderValue(): SlottedTemplateResult {
return this.statusSummary ? html`${this.statusSummary}` : nothing;
}
}

View File

@ -1,4 +1,7 @@
import { AkControlElement } from "@goauthentik/elements/AkControlElement.js";
import {
AkControlElement,
formatFormElementAsJSON,
} from "@goauthentik/elements/AkControlElement.js";
import { type Spread } from "@goauthentik/elements/types";
import { spread } from "@open-wc/lit-helpers";
@ -23,33 +26,32 @@ const hasLegalScheme = (url: string) =>
@customElement("ak-admin-settings-footer-link")
export class FooterLinkInput extends AkControlElement<FooterLink> {
static get styles() {
return [
PFBase,
PFInputGroup,
PFFormControl,
css`
.pf-c-input-group input#linkname {
flex-grow: 1;
width: 8rem;
}
`,
];
}
static styles = [
PFBase,
PFInputGroup,
PFFormControl,
css`
.pf-c-input-group input#linkname {
flex-grow: 1;
width: 8rem;
}
`,
];
@property({ type: Object, attribute: false })
footerLink: FooterLink = {
public footerLink: FooterLink = {
name: "",
href: "",
};
@queryAll(".ak-form-control")
controls?: HTMLInputElement[];
@property({ type: String })
public name?: string | null;
json() {
return Object.fromEntries(
Array.from(this.controls ?? []).map((control) => [control.name, control.value]),
) as unknown as FooterLink;
@queryAll(".ak-form-control")
protected controls?: HTMLInputElement[];
public override json() {
return formatFormElementAsJSON<FooterLink>(this.controls);
}
get isValid() {

View File

@ -1,42 +1,49 @@
import { render } from "@goauthentik/elements/tests/utils.js";
import { $, expect } from "@wdio/globals";
import { $, browser, expect } from "@wdio/globals";
import { html } from "lit";
import "../AdminSettingsFooterLinks.js";
describe("ak-admin-settings-footer-link", () => {
afterEach(async () => {
await browser.execute(async () => {
await document.body.querySelector("ak-admin-settings-footer-link")?.remove();
if (document.body._$litPart$) {
// @ts-expect-error expression of type '"_$litPart$"' is added by Lit
await delete document.body._$litPart$;
afterEach(() =>
browser.execute(() => {
document.body.querySelector("ak-admin-settings-footer-link")?.remove();
if ("_$litPart$" in document.body) {
delete document.body._$litPart$;
}
});
});
}),
);
it("should render an empty control", async () => {
render(html`<ak-admin-settings-footer-link name="link"></ak-admin-settings-footer-link>`);
const link = await $("ak-admin-settings-footer-link");
await expect(await link.getProperty("isValid")).toStrictEqual(false);
await expect(await link.getProperty("toJson")).toEqual({ name: "", href: "" });
const link = $("ak-admin-settings-footer-link");
await expect(link.getProperty("isValid")).resolves.toStrictEqual(false);
await expect(link.getProperty("toJson")).resolves.toEqual({
name: "",
href: "",
});
});
it("should not be valid if just a name is filled in", async () => {
render(html`<ak-admin-settings-footer-link name="link"></ak-admin-settings-footer-link>`);
const link = await $("ak-admin-settings-footer-link");
const link = $("ak-admin-settings-footer-link");
await link.$('input[name="name"]').setValue("foo");
await expect(await link.getProperty("isValid")).toStrictEqual(false);
await expect(await link.getProperty("toJson")).toEqual({ name: "foo", href: "" });
await expect(link.getProperty("isValid")).resolves.toStrictEqual(false);
await expect(link.getProperty("toJson")).resolves.toEqual({
name: "foo",
href: "",
});
});
it("should be valid if just a URL is filled in", async () => {
render(html`<ak-admin-settings-footer-link name="link"></ak-admin-settings-footer-link>`);
const link = await $("ak-admin-settings-footer-link");
const link = $("ak-admin-settings-footer-link");
await link.$('input[name="href"]').setValue("https://foo.com");
await expect(await link.getProperty("isValid")).toStrictEqual(true);
await expect(await link.getProperty("toJson")).toEqual({
await expect(link.getProperty("isValid")).resolves.toStrictEqual(true);
await expect(link.getProperty("toJson")).resolves.toEqual({
name: "",
href: "https://foo.com",
});
@ -44,11 +51,13 @@ describe("ak-admin-settings-footer-link", () => {
it("should be valid if both are filled in", async () => {
render(html`<ak-admin-settings-footer-link name="link"></ak-admin-settings-footer-link>`);
const link = await $("ak-admin-settings-footer-link");
const link = $("ak-admin-settings-footer-link");
await link.$('input[name="name"]').setValue("foo");
await link.$('input[name="href"]').setValue("https://foo.com");
await expect(await link.getProperty("isValid")).toStrictEqual(true);
await expect(await link.getProperty("toJson")).toEqual({
await expect(link.getProperty("isValid")).resolves.toStrictEqual(true);
await expect(link.getProperty("toJson")).resolves.toEqual({
name: "foo",
href: "https://foo.com",
});
@ -56,13 +65,13 @@ describe("ak-admin-settings-footer-link", () => {
it("should not be valid if the URL is not valid", async () => {
render(html`<ak-admin-settings-footer-link name="link"></ak-admin-settings-footer-link>`);
const link = await $("ak-admin-settings-footer-link");
const link = $("ak-admin-settings-footer-link");
await link.$('input[name="name"]').setValue("foo");
await link.$('input[name="href"]').setValue("never://foo.com");
await expect(await link.getProperty("toJson")).toEqual({
await expect(link.getProperty("toJson")).resolves.toEqual({
name: "foo",
href: "never://foo.com",
});
await expect(await link.getProperty("isValid")).toStrictEqual(false);
await expect(link.getProperty("isValid")).resolves.toStrictEqual(false);
});
});

View File

@ -177,9 +177,8 @@ export class ApplicationForm extends WithCapabilitiesConfig(ModelForm<Applicatio
.options=${policyEngineModes}
.value=${this.instance?.policyEngineMode}
></ak-radio-input>
<ak-form-group>
<span slot="header"> ${msg("UI settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("UI settings")}">
<div class="pf-c-form">
<ak-text-input
name="metaLaunchUrl"
label=${msg("Launch URL")}

View File

@ -85,7 +85,7 @@ export class ApplicationListPage extends WithBrandConfig(TablePage<Application>)
];
}
renderSidebarAfter(): TemplateResult {
protected renderSidebarAfter(): TemplateResult {
return html`<div class="pf-c-sidebar__panel pf-m-width-25">
<div class="pf-c-card">
<div class="pf-c-card__body">

View File

@ -5,7 +5,8 @@ import {
WizardNavigationEvent,
WizardUpdateEvent,
} from "@goauthentik/components/ak-wizard/events";
import { KeyUnknown, serializeForm } from "@goauthentik/elements/forms/Form";
import type { AkControlElement } from "@goauthentik/elements/forms/Form";
import { serializeForm } from "@goauthentik/elements/forms/Form";
import { HorizontalFormElement } from "@goauthentik/elements/forms/HorizontalFormElement";
import { msg } from "@lit/localize";
@ -29,22 +30,21 @@ export class ApplicationWizardStep extends WizardStep {
// As recommended in [WizardStep](../../../components/ak-wizard/WizardStep.ts), we override
// these fields and provide them to all the child classes.
wizardTitle = msg("New application");
wizardDescription = msg("Create a new application and configure a provider for it.");
canCancel = true;
protected wizardTitle = msg("New application");
protected wizardDescription = msg("Create a new application and configure a provider for it.");
public cancelable = true;
// This should be overridden in the children for more precise targeting.
@query("form")
form!: HTMLFormElement;
protected form!: HTMLFormElement;
get formValues(): KeyUnknown | undefined {
get formValues(): Record<string, unknown> {
const elements = [
...Array.from(
this.form.querySelectorAll<HorizontalFormElement>("ak-form-element-horizontal"),
),
...Array.from(this.form.querySelectorAll<HTMLElement>("[data-ak-control=true]")),
...this.form.querySelectorAll<HorizontalFormElement>("ak-form-element-horizontal"),
...this.form.querySelectorAll<AkControlElement>("[data-ak-control]"),
];
return serializeForm(elements as unknown as NodeListOf<HorizontalFormElement>);
return serializeForm(elements);
}
protected removeErrors(

View File

@ -22,7 +22,7 @@ export class AkWizardTitle extends AKElement {
render() {
return html`<div class="ak-bottom-spacing pf-c-content">
<h3><slot></slot></h3>
<h3 data-test-id="wizard-heading"><slot></slot></h3>
</div>`;
}
}
@ -33,4 +33,12 @@ declare global {
interface HTMLElementTagNameMap {
"ak-wizard-title": AkWizardTitle;
}
interface WizardTestIDMap {
heading: HTMLHeadingElement;
}
interface TestIDSelectorMap {
wizard: WizardTestIDMap;
}
}

View File

@ -1,16 +1,15 @@
import { ApplicationWizardStep } from "@goauthentik/admin/applications/wizard/ApplicationWizardStep.js";
import "@goauthentik/admin/applications/wizard/ak-wizard-title.js";
import { policyEngineModes } from "@goauthentik/admin/policies/PolicyEngineModes";
import { camelToSnake } from "@goauthentik/common/utils.js";
import "@goauthentik/components/ak-radio-input";
import "@goauthentik/components/ak-slug-input";
import "@goauthentik/components/ak-switch-input";
import "@goauthentik/components/ak-text-input";
import { type NavigableButton, type WizardButton } from "@goauthentik/components/ak-wizard/types";
import { type KeyUnknown } from "@goauthentik/elements/forms/Form";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { isSlug } from "@goauthentik/elements/router/utils.js";
import { snakeCase } from "change-case";
import { msg } from "@lit/localize";
import { html } from "lit";
@ -23,11 +22,10 @@ import { ApplicationWizardStateUpdate, ValidationRecord } from "../types";
const autoTrim = (v: unknown) => (typeof v === "string" ? v.trim() : v);
const trimMany = (o: KeyUnknown, vs: string[]) =>
const trimMany = (o: Record<string, unknown>, vs: string[]) =>
Object.fromEntries(vs.map((v) => [v, autoTrim(o[v])]));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const isStr = (v: any): v is string => typeof v === "string";
const isStr = (v: unknown): v is string => typeof v === "string";
@customElement("ak-application-wizard-application-step")
export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
@ -48,9 +46,7 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
errorMessages(name: string) {
return this.errors.has(name)
? [this.errors.get(name)]
: (this.wizard.errors?.app?.[name] ??
this.wizard.errors?.app?.[camelToSnake(name)] ??
[]);
: (this.wizard.errors?.app?.[name] ?? this.wizard.errors?.app?.[snakeCase(name)] ?? []);
}
get buttons(): WizardButton[] {
@ -146,9 +142,8 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
.value=${app.policyEngineMode}
.errorMessages=${errors.policyEngineMode ?? []}
></ak-radio-input>
<ak-form-group aria-label=${msg("UI Settings")}>
<span slot="header"> ${msg("UI Settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label=${msg("UI Settings")}>
<div class="pf-c-form">
<ak-text-input
name="metaLaunchUrl"
label=${msg("Launch URL")}

View File

@ -1,13 +1,14 @@
import { camelToSnake } from "@goauthentik/common/utils.js";
import "@goauthentik/components/ak-number-input";
import "@goauthentik/components/ak-radio-input";
import "@goauthentik/components/ak-switch-input";
import "@goauthentik/components/ak-text-input";
import { AKElement } from "@goauthentik/elements/Base.js";
import { KeyUnknown, serializeForm } from "@goauthentik/elements/forms/Form";
import type { AkControlElement } from "@goauthentik/elements/forms/Form";
import { serializeForm } from "@goauthentik/elements/forms/Form";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { HorizontalFormElement } from "@goauthentik/elements/forms/HorizontalFormElement";
import { snakeCase } from "change-case";
import { property, query } from "lit/decorators.js";
@ -30,14 +31,13 @@ export class ApplicationWizardProviderForm<T extends OneOfProvider> extends AKEl
@query("form#providerform")
form!: HTMLFormElement;
get formValues(): KeyUnknown | undefined {
get formValues(): Record<string, unknown> {
const elements = [
...Array.from(
this.form.querySelectorAll<HorizontalFormElement>("ak-form-element-horizontal"),
),
...Array.from(this.form.querySelectorAll<HTMLElement>("[data-ak-control=true]")),
...this.form.querySelectorAll<HorizontalFormElement>("ak-form-element-horizontal"),
...this.form.querySelectorAll<AkControlElement>("[data-ak-control]"),
];
return serializeForm(elements as unknown as NodeListOf<HorizontalFormElement>);
return serializeForm(elements);
}
get valid() {
@ -49,7 +49,7 @@ export class ApplicationWizardProviderForm<T extends OneOfProvider> extends AKEl
return name in this.errors
? [this.errors[name]]
: (this.wizard.errors?.provider?.[name] ??
this.wizard.errors?.provider?.[camelToSnake(name)] ??
this.wizard.errors?.provider?.[snakeCase(name)] ??
[]);
}

View File

@ -60,9 +60,8 @@ export class ApplicationWizardRACProviderForm extends ApplicationWizardProviderF
input-hint="code"
></ak-text-input>
<ak-form-group expanded>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label=" ${msg("Protocol settings")} ">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Property mappings")}
name="propertyMappings"

View File

@ -176,9 +176,8 @@ export class BlueprintForm extends ModelForm<BlueprintInstance, string> {
</div>
</div>
<ak-form-group>
<span slot="header">${msg("Additional settings")}</span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("Additional settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal label=${msg("Context")} name="context">
<ak-codemirror
mode=${CodeMirrorMode.YAML}

View File

@ -87,9 +87,8 @@ export class BrandForm extends ModelForm<Brand, string> {
</p>
</ak-form-element-horizontal>
<ak-form-group>
<span slot="header"> ${msg("Branding settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("Branding settings")} ">
<div class="pf-c-form">
<ak-form-element-horizontal label=${msg("Title")} required name="brandingTitle">
<input
type="text"
@ -170,9 +169,8 @@ export class BrandForm extends ModelForm<Brand, string> {
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("External user settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("External user settings")} ">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Default application")}
name="defaultApplication"
@ -215,9 +213,8 @@ export class BrandForm extends ModelForm<Brand, string> {
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Default flows")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("Default flows")} ">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Authentication flow")}
name="flowAuthentication"
@ -295,9 +292,8 @@ export class BrandForm extends ModelForm<Brand, string> {
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Other global settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("Other global settings")} ">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Web Certificate")}
name="webCertificate"

View File

@ -44,19 +44,18 @@ export class CoreGroupSearch extends CustomListenerElement(AKElement) {
* @attr
*/
@property({ type: String, reflect: true })
group?: string;
public group?: string;
@query("ak-search-select")
search!: SearchSelect<Group>;
public search!: SearchSelect<Group>;
@property({ type: String })
name: string | null | undefined;
public name?: string | null;
selectedGroup?: Group;
constructor() {
super();
this.selected = this.selected.bind(this);
this.handleSearchUpdate = this.handleSearchUpdate.bind(this);
}
@ -83,9 +82,9 @@ export class CoreGroupSearch extends CustomListenerElement(AKElement) {
this.dispatchEvent(new InputEvent("input", { bubbles: true, composed: true }));
}
selected(group: Group) {
selected = (group: Group) => {
return this.group === group.pk;
}
};
render() {
return html`

View File

@ -32,13 +32,19 @@ const renderValue = (item: CertificateKeyPair | undefined): string | undefined =
@customElement("ak-crypto-certificate-search")
export class AkCryptoCertificateSearch extends CustomListenerElement(AKElement) {
@property({ type: String, reflect: true })
certificate?: string;
public certificate?: string;
@query("ak-search-select")
search!: SearchSelect<CertificateKeyPair>;
public search!: SearchSelect<CertificateKeyPair>;
@property({ type: String })
name: string | null | undefined;
public name?: string | null;
@property({ type: String })
public label?: string | undefined;
@property({ type: String })
public placeholder?: string | undefined;
/**
* Set to `true` to allow certificates without private key to show up. When set to `false`,
@ -46,7 +52,7 @@ export class AkCryptoCertificateSearch extends CustomListenerElement(AKElement)
* @attr
*/
@property({ type: Boolean, attribute: "nokey" })
noKey = false;
public noKey = false;
/**
* Set this to true if, should there be only one certificate available, you want the system to
@ -55,16 +61,12 @@ export class AkCryptoCertificateSearch extends CustomListenerElement(AKElement)
* @attr
*/
@property({ type: Boolean, attribute: "singleton" })
singleton = false;
public singleton = false;
selectedKeypair?: CertificateKeyPair;
constructor() {
super();
this.selected = this.selected.bind(this);
this.fetchObjects = this.fetchObjects.bind(this);
this.handleSearchUpdate = this.handleSearchUpdate.bind(this);
}
/**
* @todo Document this.
*/
public selectedKeypair?: CertificateKeyPair;
get value() {
return this.selectedKeypair ? renderValue(this.selectedKeypair) : null;
@ -83,13 +85,13 @@ export class AkCryptoCertificateSearch extends CustomListenerElement(AKElement)
}
}
handleSearchUpdate(ev: CustomEvent) {
handleSearchUpdate = (ev: CustomEvent) => {
ev.stopPropagation();
this.selectedKeypair = ev.detail.value;
this.dispatchEvent(new InputEvent("input", { bubbles: true, composed: true }));
}
};
async fetchObjects(query?: string): Promise<CertificateKeyPair[]> {
fetchObjects = async (query?: string): Promise<CertificateKeyPair[]> => {
const args: CryptoCertificatekeypairsListRequest = {
ordering: "name",
hasKey: !this.noKey,
@ -102,19 +104,21 @@ export class AkCryptoCertificateSearch extends CustomListenerElement(AKElement)
args,
);
return certificates.results;
}
};
selected(item: CertificateKeyPair, items: CertificateKeyPair[]) {
selected = (item: CertificateKeyPair, items: CertificateKeyPair[]) => {
return (
(this.singleton && !this.certificate && items.length === 1) ||
(!!this.certificate && this.certificate === item.pk)
);
}
};
render() {
return html`
<ak-search-select
name=${ifDefined(this.name ?? undefined)}
label=${ifDefined(this.label ?? undefined)}
placeholder=${ifDefined(this.placeholder ?? undefined)}
.fetchObjects=${this.fetchObjects}
.renderElement=${renderElement}
.value=${renderValue}

View File

@ -5,6 +5,7 @@ import { SearchSelect } from "@goauthentik/elements/forms/SearchSelect";
import "@goauthentik/elements/forms/SearchSelect";
import { CustomListenerElement } from "@goauthentik/elements/utils/eventEmitter";
import { msg } from "@lit/localize";
import { html } from "lit";
import { property, query } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
@ -34,13 +35,15 @@ export function getFlowValue(flow: Flow | undefined): string | undefined {
*/
export class FlowSearch<T extends Flow> extends CustomListenerElement(AKElement) {
//#region Properties
/**
* The type of flow we're looking for.
*
* @attr
*/
@property({ type: String })
flowType?: FlowsInstancesListDesignationEnum;
public flowType?: FlowsInstancesListDesignationEnum;
/**
* The id of the current flow, if any. For stages where the flow is already defined.
@ -48,7 +51,7 @@ export class FlowSearch<T extends Flow> extends CustomListenerElement(AKElement)
* @attr
*/
@property({ type: String })
currentFlow?: string | undefined;
public currentFlow?: string | undefined;
/**
* If true, it is not valid to leave the flow blank.
@ -56,10 +59,7 @@ export class FlowSearch<T extends Flow> extends CustomListenerElement(AKElement)
* @attr
*/
@property({ type: Boolean })
required?: boolean = false;
@query("ak-search-select")
search!: SearchSelect<T>;
public required?: boolean = false;
/**
* When specified and the object instance does not have a flow selected, auto-select the flow with the given slug.
@ -70,9 +70,29 @@ export class FlowSearch<T extends Flow> extends CustomListenerElement(AKElement)
defaultFlowSlug?: string;
@property({ type: String })
name: string | null | undefined;
public name?: string | null;
selectedFlow?: T;
/**
* The label of the input, for forms.
*
* @attr
*/
@property({ type: String })
public label?: string;
/**
* The textual placeholder for the search's <input> object, if currently empty. Used as the
* native <input> object's `placeholder` field.
*
* @attr
*/
@property({ type: String })
public placeholder: string = msg("Select a flow...");
@query("ak-search-select")
protected search!: SearchSelect<T>;
protected selectedFlow?: T;
get value() {
return this.selectedFlow ? getFlowValue(this.selectedFlow) : null;
@ -80,18 +100,16 @@ export class FlowSearch<T extends Flow> extends CustomListenerElement(AKElement)
constructor() {
super();
this.fetchObjects = this.fetchObjects.bind(this);
this.selected = this.selected.bind(this);
this.handleSearchUpdate = this.handleSearchUpdate.bind(this);
}
handleSearchUpdate(ev: CustomEvent) {
handleSearchUpdate = (ev: CustomEvent) => {
ev.stopPropagation();
this.selectedFlow = ev.detail.value;
this.dispatchEvent(new InputEvent("input", { bubbles: true, composed: true }));
}
};
async fetchObjects(query?: string): Promise<Flow[]> {
fetchObjects = async (query?: string): Promise<Flow[]> => {
const args: FlowsInstancesListRequest = {
ordering: "slug",
designation: this.flowType,
@ -99,7 +117,7 @@ export class FlowSearch<T extends Flow> extends CustomListenerElement(AKElement)
};
const flows = await new FlowsApi(DEFAULT_CONFIG).flowsInstancesList(args);
return flows.results;
}
};
/* This is the most commonly overridden method of this class. About half of the Flow Searches
* use this method, but several have more complex needs, such as relating to the brand, or just
@ -134,6 +152,8 @@ export class FlowSearch<T extends Flow> extends CustomListenerElement(AKElement)
.renderElement=${renderElement}
.renderDescription=${renderDescription}
.value=${getFlowValue}
placeholder=${ifDefined(this.placeholder ?? undefined)}
label=${ifDefined(this.label ?? undefined)}
name=${ifDefined(this.name ?? undefined)}
@ak-change=${this.handleSearchUpdate}
?blankable=${!this.required}

View File

@ -21,14 +21,9 @@ export class AkBrandedFlowSearch<T extends Flow> extends FlowSearch<T> {
@property({ attribute: false, type: String })
brandFlow?: string;
constructor() {
super();
this.selected = this.selected.bind(this);
}
selected(flow: Flow): boolean {
public selected = (flow: Flow): boolean => {
return super.selected(flow) || flow.pk === this.brandFlow;
}
};
}
declare global {

View File

@ -32,19 +32,14 @@ export class AkSourceFlowSearch<T extends Flow> extends FlowSearch<T> {
@property({ type: String })
instanceId: string | undefined;
constructor() {
super();
this.selected = this.selected.bind(this);
}
// If there's no instance or no currentFlowId for it and the flow resembles the fallback,
// otherwise defer to the parent class.
selected(flow: Flow): boolean {
selected = (flow: Flow): boolean => {
return (
(!this.instanceId && !this.currentFlow && flow.slug === this.fallback) ||
super.selected(flow)
);
}
};
}
declare global {

View File

@ -8,25 +8,35 @@ import { html, nothing } from "lit";
import { customElement, property } from "lit/decorators.js";
@customElement("ak-license-notice")
export class AkLicenceNotice extends WithLicenseSummary(AKElement) {
export class AKLicenceNotice extends WithLicenseSummary(AKElement) {
static styles = [$PFBase];
@property()
notice = msg("Enterprise only");
public label = msg("Enterprise only");
@property()
public description = msg("Learn more about the enterprise license.");
render() {
return this.hasEnterpriseLicense
? nothing
: html`
<ak-alert class="pf-c-radio__description" inline plain>
<a href="#/enterprise/licenses">${this.notice}</a>
</ak-alert>
`;
if (this.hasEnterpriseLicense) {
return nothing;
}
return html`
<ak-alert class="pf-c-radio__description" inline plain>
<a
aria-label="${this.label}"
aria-description="${this.description}"
href="#/enterprise/licenses"
>${this.label}</a
>
</ak-alert>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-license-notice": AkLicenceNotice;
"ak-license-notice": AKLicenceNotice;
}
}

View File

@ -12,8 +12,8 @@ describe("ak-enterprise-status-card", () => {
it("should not error when no data is loaded", async () => {
render(html`<ak-enterprise-status-card></ak-enterprise-status-card>`);
const status = await $("ak-enterprise-status-card");
await expect(status).toHaveText(msg("Loading"));
const status = $("ak-enterprise-status-card");
await expect(status).resolves.toHaveText(msg("Loading"));
});
it("should render empty when unlicensed", async () => {
@ -35,22 +35,22 @@ describe("ak-enterprise-status-card", () => {
</ak-enterprise-status-card>`,
);
const status = await $("ak-enterprise-status-card").$(
const status = $("ak-enterprise-status-card").$(
">>>.pf-c-description-list__description > .pf-c-description-list__text",
);
await expect(status).toExist();
await expect(status).toHaveText(msg("Unlicensed"));
await expect(status).resolves.toExist();
await expect(status).resolves.toHaveText(msg("Unlicensed"));
const internalUserProgress = await $("ak-enterprise-status-card").$(
const internalUserProgress = $("ak-enterprise-status-card").$(
">>>#internalUsers > .pf-c-progress__bar",
);
await expect(internalUserProgress).toExist();
await expect(internalUserProgress).toHaveAttr("aria-valuenow", "0");
const externalUserProgress = await $("ak-enterprise-status-card").$(
await expect(internalUserProgress).resolves.toExist();
await expect(internalUserProgress).resolves.toHaveAttr("aria-valuenow", "0");
const externalUserProgress = $("ak-enterprise-status-card").$(
">>>#externalUsers > .pf-c-progress__bar",
);
await expect(externalUserProgress).toExist();
await expect(externalUserProgress).toHaveAttr("aria-valuenow", "0");
await expect(externalUserProgress).resolves.toExist();
await expect(externalUserProgress).resolves.toHaveAttr("aria-valuenow", "0");
});
it("should show warnings when full", async () => {
@ -72,34 +72,35 @@ describe("ak-enterprise-status-card", () => {
</ak-enterprise-status-card>`,
);
const status = await $("ak-enterprise-status-card").$(
const status = $("ak-enterprise-status-card").$(
">>>.pf-c-description-list__description > .pf-c-description-list__text",
);
await expect(status).toExist();
await expect(status).toHaveText(msg("Valid"));
await expect(status).resolves.toExist();
await expect(status).resolves.toHaveText(msg("Valid"));
const internalUserProgress = await $("ak-enterprise-status-card").$(
const internalUserProgress = $("ak-enterprise-status-card").$(
">>>#internalUsers > .pf-c-progress__bar",
);
await expect(internalUserProgress).toExist();
await expect(internalUserProgress).toHaveAttr("aria-valuenow", "100");
await expect(internalUserProgress).resolves.toExist();
await expect(internalUserProgress).resolves.toHaveAttr("aria-valuenow", "100");
await expect(
await $("ak-enterprise-status-card").$(">>>#internalUsers"),
).toHaveElementClass("pf-m-warning");
await expect($("ak-enterprise-status-card").$(">>>#internalUsers")).toHaveElementClass(
"pf-m-warning",
);
const externalUserProgress = await $("ak-enterprise-status-card").$(
const externalUserProgress = $("ak-enterprise-status-card").$(
">>>#externalUsers > .pf-c-progress__bar",
);
await expect(externalUserProgress).toExist();
await expect(externalUserProgress).toHaveAttr("aria-valuenow", "100");
await expect(externalUserProgress).resolves.toExist();
await expect(externalUserProgress).resolves.toHaveAttr("aria-valuenow", "100");
await expect(
await $("ak-enterprise-status-card").$(">>>#internalUsers"),
).toHaveElementClass("pf-m-warning");
$("ak-enterprise-status-card").$(">>>#internalUsers"),
).resolves.toHaveElementClass("pf-m-warning");
await expect(
await $("ak-enterprise-status-card").$(">>>#externalUsers"),
).toHaveElementClass("pf-m-warning");
$("ak-enterprise-status-card").$(">>>#externalUsers"),
).resolves.toHaveElementClass("pf-m-warning");
});
it("should show infinity when not licensed for a user type", async () => {
@ -121,33 +122,33 @@ describe("ak-enterprise-status-card", () => {
</ak-enterprise-status-card>`,
);
const status = await $("ak-enterprise-status-card").$(
const status = $("ak-enterprise-status-card").$(
">>>.pf-c-description-list__description > .pf-c-description-list__text",
);
await expect(status).toExist();
await expect(status).toHaveText(msg("Valid"));
await expect(status).resolves.toExist();
await expect(status).resolves.toHaveText(msg("Valid"));
const internalUserProgress = await $("ak-enterprise-status-card").$(
const internalUserProgress = $("ak-enterprise-status-card").$(
">>>#internalUsers > .pf-c-progress__bar",
);
await expect(internalUserProgress).toExist();
await expect(internalUserProgress).toHaveAttr("aria-valuenow", "100");
await expect(internalUserProgress).resolves.toExist();
await expect(internalUserProgress).resolves.toHaveAttr("aria-valuenow", "100");
await expect(
await $("ak-enterprise-status-card").$(">>>#internalUsers"),
).toHaveElementClass("pf-m-warning");
await expect($("ak-enterprise-status-card").$(">>>#internalUsers")).toHaveElementClass(
"pf-m-warning",
);
const externalUserProgress = await $("ak-enterprise-status-card").$(
const externalUserProgress = $("ak-enterprise-status-card").$(
">>>#externalUsers > .pf-c-progress__bar",
);
await expect(externalUserProgress).toExist();
await expect(externalUserProgress).toHaveAttr("aria-valuenow", "∞");
await expect(externalUserProgress).resolves.toExist();
await expect(externalUserProgress).resolves.toHaveAttr("aria-valuenow", "∞");
await expect(
await $("ak-enterprise-status-card").$(">>>#internalUsers"),
).toHaveElementClass("pf-m-warning");
$("ak-enterprise-status-card").$(">>>#internalUsers"),
).resolves.toHaveElementClass("pf-m-warning");
await expect(
await $("ak-enterprise-status-card").$(">>>#externalUsers"),
).toHaveElementClass("pf-m-danger");
$("ak-enterprise-status-card").$(">>>#externalUsers"),
).resolves.toHaveElementClass("pf-m-danger");
});
});

View File

@ -211,9 +211,8 @@ export class FlowForm extends WithCapabilitiesConfig(ModelForm<Flow, string>) {
${msg("Required authentication level for this flow.")}
</p>
</ak-form-element-horizontal>
<ak-form-group>
<span slot="header"> ${msg("Behavior settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("Behavior settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal name="compatibilityMode">
<label class="pf-c-switch">
<input
@ -286,9 +285,8 @@ export class FlowForm extends WithCapabilitiesConfig(ModelForm<Flow, string>) {
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Appearance settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("Appearance settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal label=${msg("Layout")} required name="layout">
<select class="pf-c-form-control">
<option

View File

@ -231,9 +231,8 @@ export class OutpostForm extends ModelForm<Outpost, string> {
selected-label="${msg("Selected Applications")}"
></ak-dual-select-provider>
</ak-form-element-horizontal>
<ak-form-group aria-label=${msg("Advanced settings")}>
<span slot="header"> ${msg("Advanced settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label=${msg("Advanced settings")}>
<div class="pf-c-form">
<ak-form-element-horizontal label=${msg("Configuration")} name="config">
<ak-codemirror
mode=${CodeMirrorMode.YAML}

View File

@ -64,9 +64,8 @@ export class DummyPolicyForm extends BasePolicyForm<DummyPolicy> {
)}
</p>
</ak-form-element-horizontal>
<ak-form-group expanded>
<span slot="header"> ${msg("Policy-specific settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Policy-specific settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal name="result">
<label class="pf-c-switch">
<input

View File

@ -76,9 +76,8 @@ export class EventMatcherPolicyForm extends BasePolicyForm<EventMatcherPolicy> {
)}
</p>
</ak-form-element-horizontal>
<ak-form-group expanded>
<span slot="header"> ${msg("Policy-specific settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Policy-specific settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal label=${msg("Action")} name="action">
<ak-search-select
.fetchObjects=${async (query?: string): Promise<TypeCreate[]> => {

View File

@ -64,9 +64,8 @@ export class PasswordExpiryPolicyForm extends BasePolicyForm<PasswordExpiryPolic
)}
</p>
</ak-form-element-horizontal>
<ak-form-group expanded>
<span slot="header"> ${msg("Policy-specific settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Policy-specific settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Maximum age (in days)")}
required

View File

@ -67,9 +67,8 @@ export class ExpressionPolicyForm extends BasePolicyForm<ExpressionPolicy> {
)}
</p>
</ak-form-element-horizontal>
<ak-form-group expanded>
<span slot="header"> ${msg("Policy-specific settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Policy-specific settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Expression")}
required

View File

@ -78,9 +78,8 @@ export class GeoIPPolicyForm extends BasePolicyForm<GeoIPPolicy> {
)}
</p>
</ak-form-element-horizontal>
<ak-form-group>
<span slot="header"> ${msg("Distance settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("Distance settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal name="checkHistoryDistance">
<label class="pf-c-switch">
<input
@ -185,9 +184,8 @@ export class GeoIPPolicyForm extends BasePolicyForm<GeoIPPolicy> {
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header">${msg("Static rule settings")}</span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("Static rule settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal label=${msg("ASNs")} name="asns">
<input
type="text"

View File

@ -44,9 +44,8 @@ export class PasswordPolicyForm extends BasePolicyForm<PasswordPolicy> {
}
renderStaticRules(): TemplateResult {
return html` <ak-form-group>
<span slot="header"> ${msg("Static rules")} </span>
<div slot="body" class="pf-c-form">
return html` <ak-form-group label="${msg("Static rules")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Minimum length")}
required
@ -142,9 +141,8 @@ export class PasswordPolicyForm extends BasePolicyForm<PasswordPolicy> {
renderHIBP(): TemplateResult {
return html`
<ak-form-group expanded>
<span slot="header"> ${msg("HaveIBeenPwned settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("HaveIBeenPwned settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Allowed count")}
required
@ -167,9 +165,8 @@ export class PasswordPolicyForm extends BasePolicyForm<PasswordPolicy> {
renderZxcvbn(): TemplateResult {
return html`
<ak-form-group expanded>
<span slot="header"> ${msg("zxcvbn settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("zxcvbn settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Score threshold")}
required

View File

@ -74,9 +74,8 @@ doesn't pass when either or both of the selected options are equal or above the
)}
</p>
</ak-form-element-horizontal>
<ak-form-group expanded>
<span slot="header"> ${msg("Policy-specific settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Policy-specific settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal name="checkIp">
<label class="pf-c-switch">
<input

View File

@ -62,9 +62,8 @@ export class PropertyMappingProviderRACForm extends BasePropertyMappingForm<RACP
required
/>
</ak-form-element-horizontal>
<ak-form-group expanded>
<span slot="header"> ${msg("General settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("General settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Username")}
name="staticSettings.username"
@ -89,9 +88,8 @@ export class PropertyMappingProviderRACForm extends BasePropertyMappingForm<RACP
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("RDP settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("RDP settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Ignore server certificate")}
name="staticSettings.ignore-cert"
@ -134,9 +132,8 @@ export class PropertyMappingProviderRACForm extends BasePropertyMappingForm<RACP
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Advanced settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("Advanced settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Expression")}
required

View File

@ -3,7 +3,7 @@ import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
import { msg } from "@lit/localize";
export abstract class BaseProviderForm<T> extends ModelForm<T, number> {
getSuccessMessage(): string {
public override getSuccessMessage(): string {
return this.instance
? msg("Successfully updated provider.")
: msg("Successfully created provider.");

View File

@ -28,32 +28,38 @@ import { Provider, ProvidersApi } from "@goauthentik/api";
@customElement("ak-provider-list")
export class ProviderListPage extends TablePage<Provider> {
searchEnabled(): boolean {
override searchEnabled(): boolean {
return true;
}
pageTitle(): string {
override pageTitle(): string {
return msg("Providers");
}
pageDescription(): string {
override pageDescription(): string {
return msg("Provide support for protocols like SAML and OAuth to assigned applications.");
}
pageIcon(): string {
override pageIcon(): string {
return "pf-icon pf-icon-integration";
}
checkbox = true;
clearOnRefresh = true;
override checkbox = true;
override clearOnRefresh = true;
@property()
order = "name";
public order = "name";
async apiEndpoint(): Promise<PaginatedResponse<Provider>> {
public searchLabel = msg("Provider name");
public searchPlaceholder = msg("Search for providers…");
override async apiEndpoint(): Promise<PaginatedResponse<Provider>> {
return new ProvidersApi(DEFAULT_CONFIG).providersAllList(
await this.defaultEndpointConfig(),
);
}
columns(): TableColumn[] {
override columns(): TableColumn[] {
return [
new TableColumn(msg("Name"), "name"),
new TableColumn(msg("Application")),
@ -62,8 +68,9 @@ export class ProviderListPage extends TablePage<Provider> {
];
}
renderToolbarSelected(): TemplateResult {
override renderToolbarSelected(): TemplateResult {
const disabled = this.selectedElements.length < 1;
return html`<ak-forms-delete-bulk
objectLabel=${msg("Provider(s)")}
.objects=${this.selectedElements}
@ -84,7 +91,7 @@ export class ProviderListPage extends TablePage<Provider> {
</ak-forms-delete-bulk>`;
}
rowApp(item: Provider): TemplateResult {
#rowApp(item: Provider): TemplateResult {
if (item.assignedApplicationName) {
return html`<i class="pf-icon pf-icon-ok pf-m-success"></i>
${msg("Assigned to application ")}
@ -92,6 +99,7 @@ export class ProviderListPage extends TablePage<Provider> {
>${item.assignedApplicationName}</a
>`;
}
if (item.assignedBackchannelApplicationName) {
return html`<i class="pf-icon pf-icon-ok pf-m-success"></i>
${msg("Assigned to application (backchannel) ")}
@ -99,15 +107,15 @@ export class ProviderListPage extends TablePage<Provider> {
>${item.assignedBackchannelApplicationName}</a
>`;
}
return html`<i class="pf-icon pf-icon-warning-triangle pf-m-warning"></i> ${msg(
"Warning: Provider not assigned to any application.",
)}`;
return html`<i aria-hidden="true" class="pf-icon pf-icon-warning-triangle pf-m-warning"></i>
${msg("Warning: Provider not assigned to any application.")}`;
}
row(item: Provider): TemplateResult[] {
override row(item: Provider): TemplateResult[] {
return [
html`<a href="#/core/providers/${item.pk}"> ${item.name} </a>`,
this.rowApp(item),
this.#rowApp(item),
html`${item.verboseName}`,
html`<ak-forms-modal>
<span slot="submit"> ${msg("Update")} </span>
@ -120,16 +128,20 @@ export class ProviderListPage extends TablePage<Provider> {
type=${item.component}
>
</ak-proxy-form>
<button slot="trigger" class="pf-c-button pf-m-plain">
<button
aria-label=${msg("Edit provider")}
slot="trigger"
class="pf-c-button pf-m-plain"
>
<pf-tooltip position="top" content=${msg("Edit")}>
<i class="fas fa-edit"></i>
<i aria-hidden="true" class="fas fa-edit"></i>
</pf-tooltip>
</button>
</ak-forms-modal>`,
];
}
renderObjectCreate(): TemplateResult {
override renderObjectCreate(): TemplateResult {
return html`<ak-provider-wizard> </ak-provider-wizard> `;
}
}

View File

@ -25,23 +25,16 @@ import { ProvidersApi, TypeCreate } from "@goauthentik/api";
@customElement("ak-provider-wizard")
export class ProviderWizard extends AKElement {
static get styles(): CSSResult[] {
return [PFBase, PFButton];
}
@property()
createText = msg("Create");
static styles: CSSResult[] = [PFBase, PFButton];
@property({ attribute: false })
providerTypes: TypeCreate[] = [];
public providerTypes: TypeCreate[] = [];
@property({ attribute: false })
finalHandler: () => Promise<void> = () => {
return Promise.resolve();
};
public finalHandler?: () => Promise<void>;
@query("ak-wizard")
wizard?: Wizard;
private wizard?: Wizard;
connectedCallback() {
super.connectedCallback();
@ -56,9 +49,7 @@ export class ProviderWizard extends AKElement {
.steps=${["initial"]}
header=${msg("New provider")}
description=${msg("Create a new provider.")}
.finalHandler=${() => {
return this.finalHandler();
}}
.finalHandler=${this.finalHandler}
>
<ak-wizard-page-type-create
name="selectProviderType"
@ -82,7 +73,15 @@ export class ProviderWizard extends AKElement {
</ak-wizard-page-form>
`;
})}
<button slot="trigger" class="pf-c-button pf-m-primary">${this.createText}</button>
<button
aria-label=${msg("New Provider")}
aria-description="${msg("Open the wizard to create a new provider.")}"
type="button"
slot="trigger"
class="pf-c-button pf-m-primary"
>
${msg("Create")}
</button>
</ak-wizard>
`;
}

View File

@ -56,9 +56,8 @@ export class GoogleWorkspaceProviderFormPage extends BaseProviderForm<GoogleWork
required
/>
</ak-form-element-horizontal>
<ak-form-group expanded>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Protocol settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Credentials")}
required
@ -181,9 +180,8 @@ export class GoogleWorkspaceProviderFormPage extends BaseProviderForm<GoogleWork
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group expanded>
<span slot="header">${msg("User filtering")}</span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("User filtering")}">
<div class="pf-c-form">
<ak-form-element-horizontal name="excludeUsersServiceAccount">
<label class="pf-c-switch">
<input
@ -234,9 +232,8 @@ export class GoogleWorkspaceProviderFormPage extends BaseProviderForm<GoogleWork
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group expanded>
<span slot="header"> ${msg("Attribute mapping")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Attribute mapping")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("User Property Mappings")}
name="propertyMappings"

View File

@ -47,7 +47,9 @@ export function renderForm(
) {
return html`
<ak-text-input
autocomplete="on"
name="name"
placeholder=${msg("Provider name")}
value=${ifDefined(provider?.name)}
label=${msg("Name")}
.errorMessages=${errors?.name ?? []}
@ -80,10 +82,8 @@ export function renderForm(
>
</ak-switch-input>
<ak-form-group expanded>
<span slot="header"> ${msg("Flow settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Flow settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Bind flow")}
required
@ -91,6 +91,7 @@ export function renderForm(
.errorMessages=${errors?.authorizationFlow ?? []}
>
<ak-branded-flow-search
label=${msg("Bind flow")}
flowType=${FlowsInstancesListDesignationEnum.Authentication}
.currentFlow=${provider?.authorizationFlow}
.brandFlow=${brand?.flowAuthentication}
@ -119,9 +120,8 @@ export function renderForm(
</div>
</ak-form-group>
<ak-form-group expanded>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Protocol settings")}">
<div class="pf-c-form">
<ak-text-input
name="baseDn"
label=${msg("Base DN")}
@ -141,6 +141,8 @@ export function renderForm(
.errorMessages=${errors?.certificate ?? []}
>
<ak-crypto-certificate-search
label=${msg("Certificate")}
placeholder=${msg("Select a certificate...")}
certificate=${ifDefined(provider?.certificate ?? nothing)}
name="certificate"
>

View File

@ -55,9 +55,8 @@ export class MicrosoftEntraProviderFormPage extends BaseProviderForm<MicrosoftEn
required
/>
</ak-form-element-horizontal>
<ak-form-group expanded>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Protocol settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal label=${msg("Client ID")} required name="clientId">
<input
type="text"
@ -157,9 +156,8 @@ export class MicrosoftEntraProviderFormPage extends BaseProviderForm<MicrosoftEn
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group expanded>
<span slot="header">${msg("User filtering")}</span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("User filtering")}">
<div class="pf-c-form">
<ak-form-element-horizontal name="excludeUsersServiceAccount">
<label class="pf-c-switch">
<input
@ -210,9 +208,8 @@ export class MicrosoftEntraProviderFormPage extends BaseProviderForm<MicrosoftEn
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group expanded>
<span slot="header"> ${msg("Attribute mapping")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Attribute mapping")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("User Property Mappings")}
name="propertyMappings"

View File

@ -2,7 +2,7 @@ import { BaseProviderForm } from "@goauthentik/admin/providers/BaseProviderForm"
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { DualSelectPair } from "@goauthentik/elements/ak-dual-select/types";
import { css } from "lit";
import { CSSResult, css } from "lit";
import { customElement, state } from "lit/decorators.js";
import { ClientTypeEnum, OAuth2Provider, ProvidersApi } from "@goauthentik/api";
@ -21,16 +21,14 @@ export async function oauth2ProvidersProvider(page = 1, search = "") {
return {
pagination: oauthProviders.pagination,
options: oauthProviders.results.map((provider) => providerToSelect(provider)),
options: oauthProviders.results.map(providerToSelect),
};
}
export function oauth2ProviderSelector(instanceProviders: number[] | undefined) {
if (!instanceProviders) {
return async (mappings: DualSelectPair<OAuth2Provider>[]) =>
mappings.filter(
([_0, _1, _2, source]: DualSelectPair<OAuth2Provider>) => source !== undefined,
);
mappings.filter(([, , , source]: DualSelectPair<OAuth2Provider>) => !source);
}
return async () => {
@ -57,41 +55,46 @@ export function oauth2ProviderSelector(instanceProviders: number[] | undefined)
@customElement("ak-provider-oauth2-form")
export class OAuth2ProviderFormPage extends BaseProviderForm<OAuth2Provider> {
@state()
showClientSecret = true;
static get styles() {
return super.styles.concat(css`
static styles: CSSResult[] = [
...super.styles,
css`
ak-array-input {
width: 100%;
}
`);
}
`,
];
async loadInstance(pk: number): Promise<OAuth2Provider> {
@state()
protected showClientSecret = true;
override async loadInstance(pk: number): Promise<OAuth2Provider> {
const provider = await new ProvidersApi(DEFAULT_CONFIG).providersOauth2Retrieve({
id: pk,
});
this.showClientSecret = provider.clientType === ClientTypeEnum.Confidential;
return provider;
}
async send(data: OAuth2Provider): Promise<OAuth2Provider> {
override async send(data: OAuth2Provider): Promise<OAuth2Provider> {
if (this.instance) {
return new ProvidersApi(DEFAULT_CONFIG).providersOauth2Update({
id: this.instance.pk,
oAuth2ProviderRequest: data,
});
}
return new ProvidersApi(DEFAULT_CONFIG).providersOauth2Create({
oAuth2ProviderRequest: data,
});
}
renderForm() {
override renderForm() {
const showClientSecretCallback = (show: boolean) => {
this.showClientSecret = show;
};
return renderForm(this.instance ?? {}, [], this.showClientSecret, showClientSecretCallback);
}
}

View File

@ -125,7 +125,9 @@ export function renderForm(
showClientSecretCallback: ShowClientSecret = defaultShowClientSecret,
) {
return html` <ak-text-input
autocomplete="on"
name="name"
placeholder=${msg("Provider name")}
label=${msg("Name")}
value=${ifDefined(provider?.name)}
required
@ -137,6 +139,8 @@ export function renderForm(
required
>
<ak-flow-search
label=${msg("Authorization flow")}
placeholder=${msg("Select an authorization flow...")}
flowType=${FlowsInstancesListDesignationEnum.Authorization}
.currentFlow=${provider?.authorizationFlow}
required
@ -145,9 +149,8 @@ export function renderForm(
${msg("Flow used when authorizing this provider.")}
</p>
</ak-form-element-horizontal>
<ak-form-group expanded>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Protocol settings")}">
<div class="pf-c-form">
<ak-radio-input
name="clientType"
label=${msg("Client type")}
@ -196,6 +199,8 @@ export function renderForm(
<ak-form-element-horizontal label=${msg("Signing Key")} name="signingKey">
<!-- NOTE: 'null' cast to 'undefined' on signingKey to satisfy Lit requirements -->
<ak-crypto-certificate-search
label=${msg("Signing Key")}
placeholder=${msg("Select a signing key...")}
certificate=${ifDefined(provider?.signingKey ?? undefined)}
singleton
></ak-crypto-certificate-search>
@ -204,6 +209,8 @@ export function renderForm(
<ak-form-element-horizontal label=${msg("Encryption Key")} name="encryptionKey">
<!-- NOTE: 'null' cast to 'undefined' on encryptionKey to satisfy Lit requirements -->
<ak-crypto-certificate-search
label=${msg("Encryption Key")}
placeholder=${msg("Select an encryption key...")}
certificate=${ifDefined(provider?.encryptionKey ?? undefined)}
></ak-crypto-certificate-search>
<p class="pf-c-form__helper-text">${msg("Key used to encrypt the tokens.")}</p>
@ -211,14 +218,15 @@ export function renderForm(
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Advanced flow settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label=${msg("Advanced flow settings")}>
<div class="pf-c-form">
<ak-form-element-horizontal
name="authenticationFlow"
label=${msg("Authentication flow")}
>
<ak-flow-search
label=${msg("Authentication flow")}
placeHolder=${msg("Select an authentication flow...")}
flowType=${FlowsInstancesListDesignationEnum.Authentication}
.currentFlow=${provider?.authenticationFlow}
></ak-flow-search>
@ -234,6 +242,8 @@ export function renderForm(
required
>
<ak-flow-search
label=${msg("Invalidation flow")}
placeHolder=${msg("Select an invalidation flow...")}
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
.currentFlow=${provider?.invalidationFlow}
defaultFlowSlug="default-provider-invalidation-flow"
@ -246,9 +256,8 @@ export function renderForm(
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Advanced protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("Advanced protocol settings")}">
<div class="pf-c-form">
<ak-text-input
name="accessCodeValidity"
label=${msg("Access code validity")}
@ -331,9 +340,8 @@ export function renderForm(
</div>
</ak-form-group>
<ak-form-group>
<span slot="header">${msg("Machine-to-Machine authentication settings")}</span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("Machine-to-Machine authentication settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Federated OIDC Sources")}
name="jwtFederationSources"

View File

@ -1,5 +1,8 @@
import "@goauthentik/admin/providers/oauth2/OAuth2ProviderRedirectURI";
import { AkControlElement } from "@goauthentik/elements/AkControlElement.js";
import {
AkControlElement,
formatFormElementAsJSON,
} from "@goauthentik/elements/AkControlElement.js";
import { type Spread } from "@goauthentik/elements/types";
import { spread } from "@open-wc/lit-helpers";
@ -43,9 +46,7 @@ export class OAuth2ProviderRedirectURI extends AkControlElement<RedirectURI> {
controls?: HTMLInputElement[];
json() {
return Object.fromEntries(
Array.from(this.controls ?? []).map((control) => [control.name, control.value]),
) as unknown as RedirectURI;
return formatFormElementAsJSON<RedirectURI>(this.controls);
}
get isValid() {

View File

@ -4,6 +4,7 @@ import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_REFRESH } from "@goauthentik/common/constants";
import renderDescriptionList from "@goauthentik/components/DescriptionList";
import "@goauthentik/components/events/ObjectChangelog";
import { IDGenerator } from "@goauthentik/core/id";
import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/EmptyState";
@ -265,12 +266,16 @@ export class OAuth2ProviderViewPage extends AKElement {
<div class="pf-c-card__body">
<form class="pf-c-form">
<div class="pf-c-form__group">
<label class="pf-c-form__label">
<label
class="pf-c-form__label"
for="${IDGenerator.elementID("providerInfo")}"
>
<span class="pf-c-form__label-text"
>${msg("OpenID Configuration URL")}</span
>
</label>
<input
id="${IDGenerator.elementID("providerInfo")}"
class="pf-c-form-control"
readonly
type="text"
@ -278,12 +283,16 @@ export class OAuth2ProviderViewPage extends AKElement {
/>
</div>
<div class="pf-c-form__group">
<label class="pf-c-form__label">
<label
class="pf-c-form__label"
for="${IDGenerator.elementID("issuer")}"
>
<span class="pf-c-form__label-text"
>${msg("OpenID Configuration Issuer")}</span
>
</label>
<input
id="${IDGenerator.elementID("issuer")}"
class="pf-c-form-control"
readonly
type="text"
@ -292,12 +301,16 @@ export class OAuth2ProviderViewPage extends AKElement {
</div>
<hr class="pf-c-divider" />
<div class="pf-c-form__group">
<label class="pf-c-form__label">
<label
class="pf-c-form__label"
for="${IDGenerator.elementID("authorize")}"
>
<span class="pf-c-form__label-text"
>${msg("Authorize URL")}</span
>
</label>
<input
id="${IDGenerator.elementID("authorize")}"
class="pf-c-form-control"
readonly
type="text"
@ -305,10 +318,14 @@ export class OAuth2ProviderViewPage extends AKElement {
/>
</div>
<div class="pf-c-form__group">
<label class="pf-c-form__label">
<label
class="pf-c-form__label"
for="${IDGenerator.elementID("token")}"
>
<span class="pf-c-form__label-text">${msg("Token URL")}</span>
</label>
<input
id="${IDGenerator.elementID("token")}"
class="pf-c-form-control"
readonly
type="text"
@ -316,12 +333,16 @@ export class OAuth2ProviderViewPage extends AKElement {
/>
</div>
<div class="pf-c-form__group">
<label class="pf-c-form__label">
<label
class="pf-c-form__label"
for="${IDGenerator.elementID("userInfo")}"
>
<span class="pf-c-form__label-text"
>${msg("Userinfo URL")}</span
>
</label>
<input
id="${IDGenerator.elementID("userInfo")}"
class="pf-c-form-control"
readonly
type="text"
@ -329,10 +350,14 @@ export class OAuth2ProviderViewPage extends AKElement {
/>
</div>
<div class="pf-c-form__group">
<label class="pf-c-form__label">
<label
class="pf-c-form__label"
for="${IDGenerator.elementID("logout")}"
>
<span class="pf-c-form__label-text">${msg("Logout URL")}</span>
</label>
<input
id="${IDGenerator.elementID("logout")}"
class="pf-c-form-control"
readonly
type="text"
@ -340,10 +365,14 @@ export class OAuth2ProviderViewPage extends AKElement {
/>
</div>
<div class="pf-c-form__group">
<label class="pf-c-form__label">
<label
class="pf-c-form__label"
for="${IDGenerator.elementID("jwks")}"
>
<span class="pf-c-form__label-text">${msg("JWKS URL")}</span>
</label>
<input
id="${IDGenerator.elementID("jwks")}"
class="pf-c-form-control"
readonly
type="text"
@ -389,9 +418,12 @@ export class OAuth2ProviderViewPage extends AKElement {
${renderDescriptionList(
[
[
msg("Preview for user"),
html`<label for="${IDGenerator.elementID("preview-user")}"
>${msg("Preview for user")}</label
>`,
html`
<ak-search-select
id="${IDGenerator.elementID("preview-user")}"
.fetchObjects=${async (query?: string): Promise<User[]> => {
const args: CoreUsersListRequest = {
ordering: "username",

View File

@ -228,9 +228,8 @@ export function renderForm(
input-hint="code"
></ak-text-input>
<ak-form-group>
<span slot="header">${msg("Advanced protocol settings")}</span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("Advanced protocol settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal label=${msg("Certificate")} name="certificate">
<ak-crypto-certificate-search
.certificate=${provider?.certificate}
@ -273,9 +272,8 @@ ${provider?.skipPathRegex}</textarea
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header">${msg("Authentication settings")}</span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("Authentication settings")}">
<div class="pf-c-form">
<ak-switch-input
name="interceptHeaderAuth"
label=${msg("Intercept header authentication")}
@ -333,9 +331,8 @@ ${provider?.skipPathRegex}</textarea
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Advanced flow settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("Advanced flow settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Authentication flow")}
name="authenticationFlow"

View File

@ -115,9 +115,8 @@ export class EndpointForm extends ModelForm<Endpoint, string> {
selected-label="${msg("Selected User Property Mappings")}"
></ak-dual-select-dynamic-selected>
</ak-form-element-horizontal>
<ak-form-group>
<span slot="header"> ${msg("Advanced settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("Advanced settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal label=${msg("Settings")} name="settings">
<ak-codemirror
mode="yaml"

View File

@ -115,9 +115,8 @@ export class RACProviderFormPage extends ModelForm<RACProvider, number> {
</p>
</ak-form-element-horizontal>
<ak-form-group expanded>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Protocol settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Property mappings")}
name="propertyMappings"

View File

@ -44,6 +44,7 @@ export function renderForm(
<ak-text-input
name="name"
label=${msg("Name")}
placeholder=${msg("Provider name")}
value=${ifDefined(provider?.name)}
.errorMessages=${errors?.name ?? []}
required
@ -57,6 +58,8 @@ export function renderForm(
.errorMessages=${errors?.authorizationFlow ?? []}
>
<ak-branded-flow-search
label=${msg("Authentication flow")}
placeholder=${msg("Select an authentication flow...")}
flowType=${FlowsInstancesListDesignationEnum.Authentication}
.currentFlow=${provider?.authorizationFlow}
.brandFlow=${brand?.flowAuthentication}
@ -73,17 +76,14 @@ export function renderForm(
>
</ak-switch-input>
<ak-form-group expanded>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-hidden-text-input
name="sharedSecret"
label=${msg("Shared secret")}
<ak-form-group open label="${msg("Protocol settings")}">
<div class="pf-c-form">
<ak-hidden-text-input>
name="sharedSecret" label=${msg("Shared secret")}
.errorMessages=${errors?.sharedSecret ?? []}
value=${provider?.sharedSecret ?? randomString(128, ascii_letters + digits)}
required
input-hint="code"
></ak-hidden-text-input>
required input-hint="code" ></ak-hidden-text-input
>
<ak-text-input
name="clientNetworks"
label=${msg("Client Networks")}
@ -106,15 +106,16 @@ export function renderForm(
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Advanced flow settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("Advanced flow settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Invalidation flow")}
name="invalidationFlow"
required
>
<ak-flow-search
label=${msg("Invalidation flow")}
placeholder=${msg("Select an invalidation flow...")}
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
.currentFlow=${provider?.invalidationFlow}
.errorMessages=${errors?.invalidationFlow ?? []}

View File

@ -84,9 +84,8 @@ export function renderForm(
</p>
</ak-form-element-horizontal>
<ak-form-group expanded>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Protocol settings")}">
<div class="pf-c-form">
<ak-text-input
name="acsUrl"
label=${msg("ACS URL")}
@ -122,9 +121,8 @@ export function renderForm(
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Advanced flow settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("Advanced flow settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Authentication flow")}
name="authenticationFlow"
@ -157,9 +155,8 @@ export function renderForm(
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Advanced protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("Advanced protocol settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal label=${msg("Signing Certificate")} name="signingKp">
<ak-crypto-certificate-search
.certificate=${provider?.signingKp}

View File

@ -31,9 +31,8 @@ export function renderForm(provider?: Partial<SCIMProvider>, errors: ValidationE
required
help=${msg("Method's display Name.")}
></ak-text-input>
<ak-form-group expanded>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Protocol settings")}">
<div class="pf-c-form">
<ak-text-input
name="url"
label=${msg("URL")}
@ -113,9 +112,8 @@ export function renderForm(provider?: Partial<SCIMProvider>, errors: ValidationE
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group expanded>
<span slot="header">${msg("User filtering")}</span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("User filtering")}">
<div class="pf-c-form">
<ak-switch-input
name="excludeUsersServiceAccount"
label=${msg("Exclude service accounts")}
@ -155,9 +153,8 @@ export function renderForm(provider?: Partial<SCIMProvider>, errors: ValidationE
</div>
</ak-form-group>
<ak-form-group expanded>
<span slot="header"> ${msg("Attribute mapping")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Attribute mapping")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("User Property Mappings")}
name="propertyMappings"

View File

@ -57,9 +57,8 @@ export class SSFProviderFormPage extends BaseProviderForm<SSFProvider> {
value=${ifDefined(provider?.name)}
required
></ak-text-input>
<ak-form-group expanded>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Protocol settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Signing Key")}
name="signingKey"
@ -93,9 +92,8 @@ export class SSFProviderFormPage extends BaseProviderForm<SSFProvider> {
</div>
</ak-form-group>
<ak-form-group>
<span slot="header">${msg("Authentication settings")}</span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("Authentication settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("OIDC Providers")}
name="oidcAuthProviders"

View File

@ -67,7 +67,7 @@ export class InitialPermissionsListPage extends TablePage<InitialPermissions> {
</ak-forms-delete-bulk>`;
}
render(): TemplateResult {
render() {
return html`<ak-page-header
icon=${this.pageIcon()}
header=${this.pageTitle()}

View File

@ -65,7 +65,7 @@ export class RoleListPage extends TablePage<Role> {
</ak-forms-delete-bulk>`;
}
render(): TemplateResult {
render() {
return html`<ak-page-header
icon=${this.pageIcon()}
header=${this.pageTitle()}

View File

@ -121,9 +121,8 @@ export class KerberosSourceForm extends WithCapabilitiesConfig(BaseSourceForm<Ke
"Enable this option to write password changes made in authentik back to Kerberos. Ignored if sync is disabled.",
)}
></ak-switch-input>
<ak-form-group expanded>
<span slot="header"> ${msg("Realm settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Realm settings")}">
<div class="pf-c-form">
<ak-text-input
name="realm"
label=${msg("Realm")}
@ -213,9 +212,8 @@ export class KerberosSourceForm extends WithCapabilitiesConfig(BaseSourceForm<Ke
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Sync connection settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("Sync connection settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("KAdmin type")}
required
@ -276,9 +274,8 @@ export class KerberosSourceForm extends WithCapabilitiesConfig(BaseSourceForm<Ke
></ak-text-input>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("SPNEGO settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("SPNEGO settings")}">
<div class="pf-c-form">
<ak-text-input
name="spnegoServerName"
label=${msg("SPNEGO server name")}
@ -305,9 +302,8 @@ export class KerberosSourceForm extends WithCapabilitiesConfig(BaseSourceForm<Ke
></ak-text-input>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Kerberos Attribute mapping")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("Kerberos Attribute mapping")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("User Property Mappings")}
name="userPropertyMappings"
@ -344,9 +340,8 @@ export class KerberosSourceForm extends WithCapabilitiesConfig(BaseSourceForm<Ke
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Flow settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("Flow settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Authentication flow")}
name="authenticationFlow"
@ -377,9 +372,8 @@ export class KerberosSourceForm extends WithCapabilitiesConfig(BaseSourceForm<Ke
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Additional settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("Additional settings")}">
<div class="pf-c-form">
<ak-text-input
name="userPathTemplate"
label=${msg("User path")}

View File

@ -171,9 +171,8 @@ export class LDAPSourceForm extends BaseSourceForm<LDAPSource> {
)}
</p>
</ak-form-element-horizontal>
<ak-form-group expanded>
<span slot="header"> ${msg("Connection settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Connection settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Server URI")}
required
@ -277,9 +276,8 @@ export class LDAPSourceForm extends BaseSourceForm<LDAPSource> {
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group expanded>
<span slot="header"> ${msg("LDAP Attribute mapping")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("LDAP Attribute mapping")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("User Property Mappings")}
name="userPropertyMappings"
@ -314,9 +312,8 @@ export class LDAPSourceForm extends BaseSourceForm<LDAPSource> {
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Additional settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("Additional settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal label=${msg("Parent Group")} name="syncParentGroup">
<ak-search-select
.fetchObjects=${async (query?: string): Promise<Group[]> => {

View File

@ -126,9 +126,8 @@ export class OAuthSourceForm extends WithCapabilitiesConfig(BaseSourceForm<OAuth
if (!this.providerType?.urlsCustomizable) {
return html``;
}
return html` <ak-form-group expanded>
<span slot="header"> ${msg("URL settings")} </span>
<div slot="body" class="pf-c-form">
return html` <ak-form-group open label="${msg("URL settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Authorization URL")}
name="authorizationUrl"
@ -421,9 +420,8 @@ export class OAuthSourceForm extends WithCapabilitiesConfig(BaseSourceForm<OAuth
<p class="pf-c-form__helper-text">${iconHelperText}</p>
</ak-form-element-horizontal>`}
<ak-form-group expanded>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Protocol settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Consumer key")}
required
@ -464,9 +462,8 @@ export class OAuthSourceForm extends WithCapabilitiesConfig(BaseSourceForm<OAuth
</div>
</ak-form-group>
${this.renderUrlOptions()}
<ak-form-group expanded>
<span slot="header"> ${msg("OAuth Attribute mapping")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("OAuth Attribute mapping")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("User Property Mappings")}
name="userPropertyMappings"
@ -501,9 +498,8 @@ export class OAuthSourceForm extends WithCapabilitiesConfig(BaseSourceForm<OAuth
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Flow settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("Flow settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Authentication flow")}
name="authenticationFlow"

View File

@ -334,9 +334,8 @@ export class PlexSourceForm extends WithCapabilitiesConfig(BaseSourceForm<PlexSo
/>
<p class="pf-c-form__helper-text">${iconHelperText}</p>
</ak-form-element-horizontal>`}
<ak-form-group expanded>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Protocol settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal label=${msg("Client ID")} required name="clientId">
<input
type="text"
@ -348,9 +347,8 @@ export class PlexSourceForm extends WithCapabilitiesConfig(BaseSourceForm<PlexSo
${this.renderSettings()}
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Flow settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("Flow settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Authentication flow")}
name="authenticationFlow"
@ -381,9 +379,8 @@ export class PlexSourceForm extends WithCapabilitiesConfig(BaseSourceForm<PlexSo
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group expanded>
<span slot="header"> ${msg("Plex Attribute mapping")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Plex Attribute mapping")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("User Property Mappings")}
name="userPropertyMappings"

View File

@ -233,9 +233,8 @@ export class SAMLSourceForm extends WithCapabilitiesConfig(BaseSourceForm<SAMLSo
<p class="pf-c-form__helper-text">${iconHelperText}</p>
</ak-form-element-horizontal>`}
<ak-form-group expanded>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Protocol settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal label=${msg("SSO URL")} required name="ssoUrl">
<input
type="text"
@ -321,9 +320,8 @@ export class SAMLSourceForm extends WithCapabilitiesConfig(BaseSourceForm<SAMLSo
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Advanced protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("Advanced protocol settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal name="allowIdpInitiated">
<label class="pf-c-switch">
<input
@ -493,9 +491,8 @@ export class SAMLSourceForm extends WithCapabilitiesConfig(BaseSourceForm<SAMLSo
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group expanded>
<span slot="header"> ${msg("SAML Attribute mapping")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("SAML Attribute mapping")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("User Property Mappings")}
name="userPropertyMappings"
@ -530,9 +527,8 @@ export class SAMLSourceForm extends WithCapabilitiesConfig(BaseSourceForm<SAMLSo
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Flow settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("Flow settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Pre-authentication flow")}
required

View File

@ -68,9 +68,8 @@ export class SCIMSourceForm extends BaseSourceForm<SCIMSource> {
<label class="pf-c-check__label"> ${msg("Enabled")} </label>
</div>
</ak-form-element-horizontal>
<ak-form-group expanded>
<span slot="header"> ${msg("SCIM Attribute mapping")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("SCIM Attribute mapping")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("User Property Mappings")}
name="userPropertyMappings"
@ -105,9 +104,8 @@ export class SCIMSourceForm extends BaseSourceForm<SCIMSource> {
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Advanced protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("Advanced protocol settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal label=${msg("User path")} name="userPathTemplate">
<input
type="text"

View File

@ -80,9 +80,8 @@ export class AuthenticatorDuoStageForm extends BaseStageForm<AuthenticatorDuoSta
required
/>
</ak-form-element-horizontal>
<ak-form-group expanded>
<span slot="header"> ${msg("Duo Auth API")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Duo Auth API")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Integration key")}
required
@ -104,15 +103,13 @@ export class AuthenticatorDuoStageForm extends BaseStageForm<AuthenticatorDuoSta
></ak-secret-text-input>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header">${msg("Duo Admin API (optional)")}</span>
<span slot="description">
${msg(
`When using a Duo MFA, Access or Beyond plan, an Admin API application can be created.
This will allow authentik to import devices automatically.`,
)}
</span>
<div slot="body" class="pf-c-form">
<ak-form-group
label=${msg("Duo Admin API (optional)")}
description="${msg(
`When using a Duo MFA, Access or Beyond plan, an Admin API application can be created. This will allow authentik to import devices automatically.`,
)}"
>
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Integration key")}
name="adminIntegrationKey"
@ -133,9 +130,8 @@ export class AuthenticatorDuoStageForm extends BaseStageForm<AuthenticatorDuoSta
></ak-secret-text-input>
</div>
</ak-form-group>
<ak-form-group expanded>
<span slot="header"> ${msg("Stage-specific settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Stage-specific settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Configuration flow")}
name="configureFlow"

View File

@ -50,9 +50,8 @@ export class AuthenticatorEmailStageForm extends BaseStageForm<AuthenticatorEmai
if (!this.showConnectionSettings) {
return html``;
}
return html`<ak-form-group expanded>
<span slot="header"> ${msg("Connection settings")} </span>
<div slot="body" class="pf-c-form">
return html`<ak-form-group open label="${msg("Connection settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal label=${msg("SMTP Host")} required name="host">
<input
type="text"
@ -191,9 +190,8 @@ export class AuthenticatorEmailStageForm extends BaseStageForm<AuthenticatorEmai
</p>
</ak-form-element-horizontal>
${this.renderConnectionSettings()}
<ak-form-group expanded>
<span slot="header"> ${msg("Stage-specific settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Stage-specific settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal label=${msg("Subject")} required name="subject">
<input
type="text"

View File

@ -55,9 +55,8 @@ export class AuthenticatorEndpointGDTCStageForm extends BaseStageForm<Authentica
required
/>
</ak-form-element-horizontal>
<ak-form-group expanded>
<span slot="header"> ${msg("Google Verified Access API")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Google Verified Access API")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Credentials")}
required

View File

@ -222,9 +222,8 @@ export class AuthenticatorSMSStageForm extends BaseStageForm<AuthenticatorSMSSta
)}
</p>
</ak-form-element-horizontal>
<ak-form-group expanded>
<span slot="header"> ${msg("Stage-specific settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Stage-specific settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal label=${msg("Provider")} required name="provider">
<select
class="pf-c-form-control"

View File

@ -67,9 +67,8 @@ export class AuthenticatorStaticStageForm extends BaseStageForm<AuthenticatorSta
)}
</p>
</ak-form-element-horizontal>
<ak-form-group expanded>
<span slot="header"> ${msg("Stage-specific settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Stage-specific settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Token count")}
required

View File

@ -69,9 +69,8 @@ export class AuthenticatorTOTPStageForm extends BaseStageForm<AuthenticatorTOTPS
)}
</p>
</ak-form-element-horizontal>
<ak-form-group expanded>
<span slot="header"> ${msg("Stage-specific settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Stage-specific settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal label=${msg("Digits")} required name="digits">
<select name="users" class="pf-c-form-control">
<option

View File

@ -95,9 +95,8 @@ export class AuthenticatorValidateStageForm extends BaseStageForm<AuthenticatorV
required
/>
</ak-form-element-horizontal>
<ak-form-group expanded>
<span slot="header"> ${msg("Stage-specific settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Stage-specific settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Device classes")}
required
@ -208,9 +207,8 @@ export class AuthenticatorValidateStageForm extends BaseStageForm<AuthenticatorV
: html``}
</div>
</ak-form-group>
<ak-form-group expanded>
<span slot="header"> ${msg("WebAuthn-specific settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("WebAuthn-specific settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("WebAuthn User verification")}
required

View File

@ -77,9 +77,8 @@ export class AuthenticatorWebAuthnStageForm extends BaseStageForm<AuthenticatorW
)}
</p>
</ak-form-element-horizontal>
<ak-form-group expanded>
<span slot="header"> ${msg("Stage-specific settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Stage-specific settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("User verification")}
required

View File

@ -47,9 +47,8 @@ export class CaptchaStageForm extends BaseStageForm<CaptchaStage> {
required
/>
</ak-form-element-horizontal>
<ak-form-group expanded>
<span slot="header"> ${msg("Stage-specific settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Stage-specific settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Public Key")}
required
@ -126,9 +125,8 @@ export class CaptchaStageForm extends BaseStageForm<CaptchaStage> {
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Advanced settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("Advanced settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal label=${msg("JS URL")} required name="jsUrl">
<input
type="url"

View File

@ -53,9 +53,8 @@ export class ConsentStageForm extends BaseStageForm<ConsentStage> {
required
/>
</ak-form-element-horizontal>
<ak-form-group expanded>
<span slot="header"> ${msg("Stage-specific settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Stage-specific settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal label=${msg("Mode")} required name="mode">
<select
class="pf-c-form-control"

View File

@ -44,9 +44,8 @@ export class DenyStageForm extends BaseStageForm<DenyStage> {
required
/>
</ak-form-element-horizontal>
<ak-form-group expanded>
<span slot="header"> ${msg("Stage-specific settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Stage-specific settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal label=${msg("Deny message")} name="denyMessage">
<input
type="text"

View File

@ -47,9 +47,8 @@ export class EmailStageForm extends BaseStageForm<EmailStage> {
if (!this.showConnectionSettings) {
return html``;
}
return html`<ak-form-group>
<span slot="header"> ${msg("Connection settings")} </span>
<div slot="body" class="pf-c-form">
return html`<ak-form-group label="${msg("Connection settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal label=${msg("SMTP Host")} required name="host">
<input
type="text"
@ -146,9 +145,8 @@ export class EmailStageForm extends BaseStageForm<EmailStage> {
required
/>
</ak-form-element-horizontal>
<ak-form-group expanded>
<span slot="header"> ${msg("Stage-specific settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Stage-specific settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal name="activateUserOnSuccess">
<label class="pf-c-switch">
<input

View File

@ -84,9 +84,8 @@ export class IdentificationStageForm extends BaseStageForm<IdentificationStage>
required
/>
</ak-form-element-horizontal>
<ak-form-group expanded>
<span slot="header"> ${msg("Stage-specific settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Stage-specific settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal label=${msg("User fields")} name="userFields">
<ak-checkbox-group
class="user-field-select"
@ -193,9 +192,8 @@ export class IdentificationStageForm extends BaseStageForm<IdentificationStage>
></ak-switch-input>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Source settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("Source settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal label=${msg("Sources")} required name="sources">
<ak-dual-select-dynamic-selected
.provider=${sourcesProvider}
@ -231,9 +229,8 @@ export class IdentificationStageForm extends BaseStageForm<IdentificationStage>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header">${msg("Flow settings")}</span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("Flow settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Passwordless flow")}
name="passwordlessFlow"

View File

@ -172,7 +172,7 @@ export class InvitationListPage extends TablePage<Invitation> {
`;
}
render(): TemplateResult {
render() {
return html`<ak-page-header
icon=${this.pageIcon()}
header=${this.pageTitle()}

View File

@ -41,9 +41,8 @@ export class InvitationStageForm extends BaseStageForm<InvitationStage> {
required
/>
</ak-form-element-horizontal>
<ak-form-group expanded>
<span slot="header"> ${msg("Stage-specific settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Stage-specific settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal name="continueFlowWithoutInvitation">
<label class="pf-c-switch">
<input

View File

@ -53,9 +53,8 @@ export class MTLSStageForm extends BaseStageForm<MutualTLSStage> {
required
/>
</ak-form-element-horizontal>
<ak-form-group expanded>
<span slot="header"> ${msg("Stage-specific settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Stage-specific settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal label=${msg("Mode")} required name="mode">
<ak-radio
.options=${[

View File

@ -82,9 +82,8 @@ export class PasswordStageForm extends BaseStageForm<PasswordStage> {
required
/>
</ak-form-element-horizontal>
<ak-form-group expanded>
<span slot="header"> ${msg("Stage-specific settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Stage-specific settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal label=${msg("Backends")} required name="backends">
<ak-checkbox-group
class="user-field-select"

View File

@ -55,9 +55,8 @@ export class PromptStageForm extends BaseStageForm<PromptStage> {
required
/>
</ak-form-element-horizontal>
<ak-form-group expanded>
<span slot="header"> ${msg("Stage-specific settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Stage-specific settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal label=${msg("Fields")} required name="fields">
<ak-dual-select-dynamic-selected
.provider=${promptFieldsProvider}

View File

@ -56,9 +56,8 @@ export class RedirectStageForm extends BaseStageForm<RedirectStage> {
required
/>
</ak-form-element-horizontal>
<ak-form-group expanded>
<span slot="header"> ${msg("Stage-specific settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Stage-specific settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal label=${msg("Mode")} required name="mode">
<select
class="pf-c-form-control"

View File

@ -41,9 +41,8 @@ export class UserLoginStageForm extends BaseStageForm<UserLoginStage> {
required
/>
</ak-form-element-horizontal>
<ak-form-group expanded>
<span slot="header"> ${msg("Stage-specific settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Stage-specific settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Session duration")}
required

View File

@ -55,9 +55,8 @@ export class UserWriteStageForm extends BaseStageForm<UserWriteStage> {
required
/>
</ak-form-element-horizontal>
<ak-form-group expanded>
<span slot="header"> ${msg("Stage-specific settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Stage-specific settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal name="userCreationMode">
<ak-radio
.options=${[

View File

@ -399,7 +399,7 @@ export class UserListPage extends WithBrandConfig(WithCapabilitiesConfig(TablePa
`;
}
renderSidebarBefore(): TemplateResult {
protected renderSidebarBefore(): TemplateResult {
return html`<div class="pf-c-sidebar__panel pf-m-width-25">
<div class="pf-c-card">
<div class="pf-c-card__title">${msg("User folders")}</div>

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