Compare commits
1 Commits
main
...
webdriver-
Author | SHA1 | Date | |
---|---|---|---|
e4c8c79ed4 |
@ -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
2
web/.gitignore
vendored
@ -25,6 +25,8 @@ lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
playwright-report
|
||||
test-results
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
|
@ -27,11 +27,6 @@ const inlineCSSPlugin = {
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @satisfies {InlineConfig}
|
||||
*/
|
||||
// const viteFinal = ;
|
||||
|
||||
/**
|
||||
* @satisfies {StorybookConfig}
|
||||
*/
|
||||
|
89
web/e2e/elements/proxy.ts
Normal file
89
web/e2e/elements/proxy.ts
Normal 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>;
|
||||
}
|
174
web/e2e/fixtures/FormFixture.ts
Normal file
174
web/e2e/fixtures/FormFixture.ts
Normal 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
|
||||
}
|
29
web/e2e/fixtures/PageFixture.ts
Normal file
29
web/e2e/fixtures/PageFixture.ts
Normal 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);
|
||||
}
|
||||
}
|
41
web/e2e/fixtures/PointerFixture.ts
Normal file
41
web/e2e/fixtures/PointerFixture.ts
Normal 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()
|
||||
);
|
||||
};
|
||||
}
|
118
web/e2e/fixtures/SessionFixture.ts
Normal file
118
web/e2e/fixtures/SessionFixture.ts
Normal 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
55
web/e2e/index.ts
Normal 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
44
web/e2e/selectors/ouid.ts
Normal 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;
|
||||
},
|
||||
};
|
||||
}
|
13
web/e2e/selectors/types.ts
Normal file
13
web/e2e/selectors/types.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import type { Locator } from "@playwright/test";
|
||||
|
||||
export type LocatorContext = Pick<
|
||||
Locator,
|
||||
| "locator"
|
||||
| "getByRole"
|
||||
| "getByTestId"
|
||||
| "getByText"
|
||||
| "getByLabel"
|
||||
| "getByAltText"
|
||||
| "getByTitle"
|
||||
| "getByPlaceholder"
|
||||
>;
|
59
web/e2e/utils/generators.ts
Normal file
59
web/e2e/utils/generators.ts
Normal 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
101
web/logger/node.js
Normal 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
21
web/logger/transport.js
Normal 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
8555
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||
],
|
||||
|
50
web/packages/core/id/index.js
Normal file
50
web/packages/core/id/index.js
Normal 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("");
|
||||
}
|
||||
}
|
27
web/packages/core/promises/index.js
Normal file
27
web/packages/core/promises/index.js
Normal 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);
|
||||
}
|
||||
}
|
2
web/packages/core/types/node.d.ts
vendored
2
web/packages/core/types/node.d.ts
vendored
@ -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
92
web/playwright.config.js
Normal 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"],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
@ -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">
|
||||
|
@ -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>`;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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> ${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> ${msg("Failed to fetch")}</p>
|
||||
<p><i aria-hidden="true" class="fa fa-times"></i> ${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">
|
||||
${
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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() {
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -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")}
|
||||
|
@ -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">
|
||||
|
@ -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(
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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")}
|
||||
|
@ -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)] ??
|
||||
[]);
|
||||
}
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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}
|
||||
|
@ -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"
|
||||
|
@ -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`
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
|
@ -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
|
||||
|
@ -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[]> => {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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.");
|
||||
|
@ -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> `;
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
`;
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
>
|
||||
|
@ -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"
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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() {
|
||||
|
@ -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",
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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 ?? []}
|
||||
|
@ -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}
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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()}
|
||||
|
@ -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()}
|
||||
|
@ -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")}
|
||||
|
@ -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[]> => {
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -172,7 +172,7 @@ export class InvitationListPage extends TablePage<Invitation> {
|
||||
`;
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
render() {
|
||||
return html`<ak-page-header
|
||||
icon=${this.pageIcon()}
|
||||
header=${this.pageTitle()}
|
||||
|
@ -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
|
||||
|
@ -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=${[
|
||||
|
@ -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"
|
||||
|
@ -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}
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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=${[
|
||||
|
@ -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
Reference in New Issue
Block a user