Merge branch 'main' into celery-2-dramatiq

This commit is contained in:
Marc 'risson' Schmitt
2025-05-26 18:29:26 +02:00
682 changed files with 40181 additions and 20687 deletions

View File

@ -2,15 +2,11 @@
node_modules
# don't lint build output (make sure it's set to your correct build folder name)
dist
out
# don't lint nyc coverage output
coverage
# Import order matters
poly.ts
src/locale-codes.ts
src/locales/
storybook-static/
# Prettier breaks the tsconfig file
tsconfig.json
.storybook/css-import-maps*
package.json
packages/**/package.json

View File

@ -1,11 +0,0 @@
import { create } from "@storybook/theming/create";
const isDarkMode = window.matchMedia("(prefers-color-scheme: dark)").matches;
export default create({
base: isDarkMode ? "dark" : "light",
brandTitle: "authentik Storybook",
brandUrl: "https://goauthentik.io",
brandImage: "https://goauthentik.io/img/icon_left_brand_colour.svg",
brandTarget: "_self",
});

70
web/.storybook/main.js Normal file
View File

@ -0,0 +1,70 @@
/**
* @file Storybook configuration.
* @import { StorybookConfig } from "@storybook/web-components-vite";
* @import { InlineConfig, Plugin } from "vite";
*/
import postcssLit from "rollup-plugin-postcss-lit";
import tsconfigPaths from "vite-tsconfig-paths";
const CSSImportPattern = /import [\w$]+ from .+\.(css)/g;
const JavaScriptFilePattern = /\.m?(js|ts|tsx)$/;
/**
* @satisfies {Plugin<never>}
*/
const inlineCSSPlugin = {
name: "inline-css-plugin",
transform: (source, id) => {
if (!JavaScriptFilePattern.test(id)) return;
const code = source.replace(CSSImportPattern, (match) => {
return `${match}?inline`;
});
return {
code,
};
},
};
/**
* @satisfies {InlineConfig}
*/
// const viteFinal = ;
/**
* @satisfies {StorybookConfig}
*/
const config = {
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
addons: [
"@storybook/addon-controls",
"@storybook/addon-links",
"@storybook/addon-essentials",
"storybook-addon-mock",
],
framework: {
name: "@storybook/web-components-vite",
options: {},
},
docs: {
autodocs: "tag",
},
async viteFinal(config) {
const [{ mergeConfig }, { createBundleDefinitions }] = await Promise.all([
import("vite"),
import("@goauthentik/web/bundler/utils/node"),
]);
/**
* @satisfies {InlineConfig}
*/
const overrides = {
define: createBundleDefinitions(),
plugins: [inlineCSSPlugin, postcssLit(), tsconfigPaths()],
};
return mergeConfig(config, overrides);
},
};
export default config;

View File

@ -1,81 +0,0 @@
import replace from "@rollup/plugin-replace";
import type { StorybookConfig } from "@storybook/web-components-vite";
import { cwd } from "process";
import modify from "rollup-plugin-modify";
import postcssLit from "rollup-plugin-postcss-lit";
import tsconfigPaths from "vite-tsconfig-paths";
export const isProdBuild = process.env.NODE_ENV === "production";
export const apiBasePath = process.env.AK_API_BASE_PATH || "";
const importInlinePatterns = [
'import AKGlobal from "(\\.\\./)*common/styles/authentik\\.css',
'import AKGlobal from "@goauthentik/common/styles/authentik\\.css',
'import PF.+ from "@patternfly/patternfly/\\S+\\.css',
'import ThemeDark from "@goauthentik/common/styles/theme-dark\\.css',
'import OneDark from "@goauthentik/common/styles/one-dark\\.css',
'import styles from "\\./LibraryPageImpl\\.css',
];
const importInlineRegexp = new RegExp(importInlinePatterns.map((a) => `(${a})`).join("|"));
const config: StorybookConfig = {
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
addons: [
"@storybook/addon-controls",
"@storybook/addon-links",
"@storybook/addon-essentials",
"storybook-addon-mock",
],
staticDirs: [
{
from: "../node_modules/@patternfly/patternfly/patternfly-base.css",
to: "@patternfly/patternfly/patternfly-base.css",
},
{
from: "../src/common/styles/authentik.css",
to: "@goauthentik/common/styles/authentik.css",
},
{
from: "../src/common/styles/theme-dark.css",
to: "@goauthentik/common/styles/theme-dark.css",
},
{
from: "../src/common/styles/one-dark.css",
to: "@goauthentik/common/styles/one-dark.css",
},
],
framework: {
name: "@storybook/web-components-vite",
options: {},
},
docs: {
autodocs: "tag",
},
async viteFinal(config) {
return {
...config,
plugins: [
modify({
find: importInlineRegexp,
replace: (match: RegExpMatchArray) => {
return `${match}?inline`;
},
}),
replace({
"process.env.NODE_ENV": JSON.stringify(
isProdBuild ? "production" : "development",
),
"process.env.CWD": JSON.stringify(cwd()),
"process.env.AK_API_BASE_PATH": JSON.stringify(apiBasePath),
"preventAssignment": true,
}),
...config.plugins,
postcssLit(),
tsconfigPaths(),
],
};
},
};
export default config;

38
web/.storybook/manager.js Normal file
View File

@ -0,0 +1,38 @@
/**
* @file Storybook manager configuration.
*
* @import { ThemeVarsPartial } from "storybook/internal/theming";
*/
import { createUIThemeEffect, resolveUITheme } from "@goauthentik/web/common/theme.ts";
import { addons } from "@storybook/manager-api";
import { create } from "@storybook/theming/create";
/**
* @satisfies {Partial<ThemeVarsPartial>}
*/
const baseTheme = {
brandTitle: "authentik Storybook",
brandUrl: "https://goauthentik.io",
brandImage: "https://goauthentik.io/img/icon_left_brand_colour.svg",
brandTarget: "_self",
};
const uiTheme = resolveUITheme();
addons.setConfig({
theme: create({
...baseTheme,
base: uiTheme,
}),
enableShortcuts: false,
});
createUIThemeEffect((nextUITheme) => {
addons.setConfig({
theme: create({
...baseTheme,
base: nextUITheme,
}),
enableShortcuts: false,
});
});

View File

@ -1,9 +0,0 @@
// .storybook/manager.js
import { addons } from "@storybook/manager-api";
import authentikTheme from "./authentikTheme";
addons.setConfig({
theme: authentikTheme,
enableShortcuts: false,
});

View File

@ -1,5 +1,3 @@
<link rel="stylesheet" href="@patternfly/patternfly/patternfly-base.css" />
<link rel="stylesheet" href="@goauthentik/common/styles/authentik.css" />
<style>
body {
overflow-y: scroll;

32
web/.storybook/preview.js Normal file
View File

@ -0,0 +1,32 @@
/// <reference types="../types/css.js" />
/**
* @file Storybook manager configuration.
*
* @import { Preview } from "@storybook/web-components";
*/
import { applyDocumentTheme } from "@goauthentik/web/common/theme.ts";
applyDocumentTheme();
/**
* @satisfies {Preview}
*/
const preview = {
parameters: {
options: {
storySort: {
method: "alphabetical",
},
},
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
},
};
export default preview;

View File

@ -1,30 +0,0 @@
import type { Preview } from "@storybook/web-components";
import "@goauthentik/common/styles/authentik.css";
// import "@goauthentik/common/styles/theme-dark.css";
import "@patternfly/patternfly/components/Brand/brand.css";
import "@patternfly/patternfly/components/Page/page.css";
// .storybook/preview.js
import "@patternfly/patternfly/patternfly-base.css";
const preview: Preview = {
parameters: {
options: {
storySort: {
method: "alphabetical",
},
},
actions: { argTypesRegex: "^on[A-Z].*" },
cssUserPrefs: {
"prefers-color-scheme": "light",
},
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
},
};
export default preview;

View File

@ -0,0 +1,96 @@
/**
* @file MDX plugin for ESBuild.
*
* @import {
* OnLoadArgs,
* OnLoadResult,
* OnResolveArgs,
* OnResolveResult,
* Plugin,
* PluginBuild
* } from "esbuild"
*/
import { MonoRepoRoot } from "@goauthentik/core/paths/node";
import * as fs from "node:fs/promises";
import * as path from "node:path";
/**
* @typedef {Omit<OnLoadArgs, 'pluginData'> & LoadDataFields} LoadData Data passed to `onload`.
*
* @typedef LoadDataFields Extra fields given in `data` to `onload`.
* @property {PluginData | null | undefined} [pluginData] Plugin data.
*
* @typedef PluginData Extra data passed.
* @property {Buffer | string | null | undefined} [contents] File contents.
*/
const pluginName = "mdx-plugin";
/**
* @typedef MDXPluginOptions
*
* @property {string} root Root directory.
*/
/**
* Bundle MDX into JSON modules.
*
* @param {MDXPluginOptions} options
* @returns {Plugin}
*/
export function mdxPlugin({ root }) {
// TODO: Replace with `resolvePackage` after NPM Workspaces support is added.
const docsPackageRoot = path.resolve(MonoRepoRoot, "website");
/**
* @param {PluginBuild} build
*/
function setup(build) {
/**
* @param {OnResolveArgs} args
* @returns {Promise<OnResolveResult>}
*/
async function resolveListener(args) {
if (!args.path.startsWith("~")) return args;
return {
path: path.resolve(docsPackageRoot, args.path.slice(1)),
pluginName,
};
}
/**
* @param {LoadData} data
* @returns {Promise<OnLoadResult>}
*/
async function loadListener(data) {
const content = String(
data.pluginData &&
data.pluginData.contents !== null &&
data.pluginData.contents !== undefined
? data.pluginData.contents
: await fs.readFile(data.path),
);
const publicPath = path.resolve(
"/",
path.relative(path.join(root, "website"), data.path),
);
const publicDirectory = path.dirname(publicPath);
return {
contents: JSON.stringify({ content, publicPath, publicDirectory }),
loader: "file",
pluginName,
};
}
build.onResolve({ filter: /\.mdx?$/ }, resolveListener);
build.onLoad({ filter: /\.mdx?$/ }, loadListener);
}
return {
name: pluginName,
setup,
};
}

29
web/bundler/utils/node.js Normal file
View File

@ -0,0 +1,29 @@
/**
* @file Bundler utilities.
*/
import { NodeEnvironment, serializeEnvironmentVars } from "@goauthentik/core/environment/node";
import { AuthentikVersion } from "@goauthentik/core/version/node";
/**
* Creates a mapping of environment variables to their respective runtime constants.
*/
export function createBundleDefinitions() {
const SerializedNodeEnvironment = /** @type {`"development"` | `"production"`} */ (
JSON.stringify(NodeEnvironment)
);
/**
* @satisfies {Record<ESBuildImportEnvKey, string>}
*/
const envRecord = {
AK_VERSION: AuthentikVersion,
AK_API_BASE_PATH: process.env.AK_API_BASE_PATH ?? "",
};
return {
...serializeEnvironmentVars(envRecord),
// We need to explicitly set this for NPM packages that use `process`
// to determine their environment.
"process.env.NODE_ENV": SerializedNodeEnvironment,
};
}

38
web/commands.mjs Normal file
View File

@ -0,0 +1,38 @@
/// <reference types="@wdio/globals/types" />
/// <reference types="./types/webdriver.js" />
/**
*
* @param {WebdriverIO.Browser} browser
*/
export function addCommands(browser) {
/**
* @file Custom WDIO browser commands
*/
browser.addCommand(
"focus",
/**
* @this {HTMLElement}
*/
function () {
this.focus();
return this;
},
/* attachToElement */ true,
);
browser.addCommand(
"blur",
/**
* @this {HTMLElement}
*/
function () {
this.blur();
return this;
},
/* attachToElement */ true,
);
}

View File

@ -1,90 +1,56 @@
import eslint from "@eslint/js";
import tsparser from "@typescript-eslint/parser";
import litconf from "eslint-plugin-lit";
import wcconf from "eslint-plugin-wc";
import globals from "globals";
import { createESLintPackageConfig } from "@goauthentik/eslint-config";
import tseslint from "typescript-eslint";
export default [
// You would not believe how much this change has frustrated users: ["if an ignores key is used
// without any other keys in the configuration object, then the patterns act as global
// ignores"](https://eslint.org/docs/latest/use/configure/ignore)
// @ts-check
/**
* ESLint configuration for authentik's monorepo.
*/
const ESLintConfig = createESLintPackageConfig({
ignorePatterns: [
"**/dist/**",
"**/out/**",
"**/.wireit/**",
"**/node_modules/",
"**/.storybook/*",
"coverage/",
"src/locale-codes.ts",
"storybook-static/",
"src/locales/",
],
});
export default tseslint.config(
...ESLintConfig,
{
ignores: [
"dist/",
// don't lint the cache
".wireit/",
// let packages have their own configurations
"packages/",
// don't ever lint node_modules
"node_modules/",
".storybook/*",
// don't lint build output (make sure it's set to your correct build folder name)
// don't lint nyc coverage output
"coverage/",
"src/locale-codes.ts",
"storybook-static/",
"src/locales/",
],
},
eslint.configs.recommended,
wcconf.configs["flat/recommended"],
litconf.configs["flat/recommended"],
...tseslint.configs.recommended,
{
languageOptions: {
parser: tsparser,
parserOptions: {
ecmaVersion: 12,
sourceType: "module",
},
},
files: ["src/**"],
rules: {
"lit/attribute-names": "off",
// "lit/attribute-names": "error",
"lit/no-private-properties": "error",
// "lit/prefer-nothing": "warn",
"lit/no-template-bind": "error",
"no-unused-vars": "off",
"no-console": ["error", { allow: ["debug", "warn", "error"] }],
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
caughtErrorsIgnorePattern: "^_",
},
],
},
},
{
languageOptions: {
parser: tsparser,
parserOptions: {
ecmaVersion: 12,
sourceType: "module",
},
globals: {
...globals.nodeBuiltin,
...globals.node,
},
},
files: ["scripts/**/*.mjs", "*.ts", "*.mjs"],
rules: {
"no-unused-vars": "off",
// We WANT our scripts to output to the console!
"no-console": "off",
},
files: ["packages/**/*"],
},
{
rules: {
"no-void": "off",
"no-implicit-coercion": "off",
"prefer-template": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
caughtErrorsIgnorePattern: "^_",
},
],
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/no-use-before-define": "off",
"array-callback-return": "off",
"block-scoped-var": "off",
"consistent-return": "off",
"func-names": "off",
"guard-for-in": "off",
"no-bitwise": "off",
"no-div-regex": "off",
"no-else-return": "off",
"no-empty-function": "off",
"no-param-reassign": "off",
"no-throw-literal": "off",
// "no-var": "off",
"prefer-arrow-callback": "off",
"react/jsx-no-leaked-render": "off",
"vars-on-top": "off",
},
},
];
);

13521
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,167 +1,27 @@
{
"name": "@goauthentik/web",
"version": "0.0.0",
"dependencies": {
"@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-html": "^6.4.9",
"@codemirror/lang-javascript": "^6.2.2",
"@codemirror/lang-python": "^6.1.6",
"@codemirror/lang-xml": "^6.1.0",
"@codemirror/legacy-modes": "^6.4.1",
"@codemirror/theme-one-dark": "^6.1.2",
"@floating-ui/dom": "^1.6.11",
"@formatjs/intl-listformat": "^7.5.7",
"@fortawesome/fontawesome-free": "^6.6.0",
"@goauthentik/api": "^2025.2.4-1745325566",
"@lit-labs/ssr": "^3.2.2",
"@lit/context": "^1.1.2",
"@lit/localize": "^0.12.2",
"@lit/reactive-element": "^2.0.4",
"@lit/task": "^1.0.1",
"@mdx-js/mdx": "^3.1.0",
"@open-wc/lit-helpers": "^0.7.0",
"@patternfly/elements": "^4.0.2",
"@patternfly/patternfly": "^4.224.2",
"@sentry/browser": "^8.32.0",
"@spotlightjs/spotlight": "^2.4.2",
"@webcomponents/webcomponentsjs": "^2.8.0",
"base64-js": "^1.5.1",
"change-case": "^5.4.4",
"chart.js": "^4.4.4",
"chartjs-adapter-date-fns": "^3.0.0",
"codemirror": "^6.0.1",
"construct-style-sheets-polyfill": "^3.1.0",
"core-js": "^3.38.1",
"country-flag-icons": "^1.5.13",
"date-fns": "^4.1.0",
"deepmerge-ts": "^7.1.5",
"dompurify": "^3.2.4",
"fuse.js": "^7.0.0",
"guacamole-common-js": "^1.5.0",
"hastscript": "^9.0.1",
"lit": "^3.2.0",
"md-front-matter": "^1.0.4",
"mermaid": "^11.4.1",
"rapidoc": "^9.3.8",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"rehype-highlight": "^7.0.2",
"rehype-mermaid": "^3.0.0",
"rehype-parse": "^9.0.1",
"rehype-stringify": "^10.0.1",
"remark-directive": "^4.0.0",
"remark-frontmatter": "^5.0.0",
"remark-gfm": "^4.0.1",
"remark-mdx-frontmatter": "^5.0.0",
"style-mod": "^4.1.2",
"ts-pattern": "^5.4.0",
"unist-util-visit": "^5.0.0",
"webcomponent-qr-code": "^1.2.0",
"yaml": "^2.5.1"
},
"devDependencies": {
"@eslint/js": "^9.11.1",
"@goauthentik/esbuild-plugin-live-reload": "^1.0.4",
"@goauthentik/prettier-config": "^1.0.4",
"@goauthentik/tsconfig": "^1.0.4",
"@hcaptcha/types": "^1.0.4",
"@lit/localize-tools": "^0.8.0",
"@rollup/plugin-replace": "^6.0.1",
"@storybook/addon-essentials": "^8.3.4",
"@storybook/addon-links": "^8.3.4",
"@storybook/api": "^7.6.17",
"@storybook/blocks": "^8.3.4",
"@storybook/builder-vite": "^8.3.4",
"@storybook/manager-api": "^8.3.4",
"@storybook/web-components": "^8.3.4",
"@storybook/web-components-vite": "^8.3.4",
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
"@types/chart.js": "^2.9.41",
"@types/codemirror": "^5.60.15",
"@types/dompurify": "^3.0.5",
"@types/eslint__js": "^8.42.3",
"@types/grecaptcha": "^3.0.9",
"@types/guacamole-common-js": "^1.5.2",
"@types/mocha": "^10.0.8",
"@types/node": "^22.7.4",
"@types/react": "^18.3.13",
"@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^8.8.0",
"@typescript-eslint/parser": "^8.8.0",
"@wdio/browser-runner": "9.4",
"@wdio/cli": "9.4",
"@wdio/spec-reporter": "^9.1.2",
"chromedriver": "^131.0.1",
"esbuild": "^0.25.0",
"esbuild-plugin-polyfill-node": "^0.3.0",
"esbuild-plugins-node-modules-polyfill": "^1.7.0",
"eslint": "^9.11.1",
"eslint-plugin-lit": "^1.15.0",
"eslint-plugin-wc": "^2.1.1",
"github-slugger": "^2.0.0",
"glob": "^11.0.0",
"globals": "^15.10.0",
"knip": "^5.30.6",
"lit-analyzer": "^2.0.3",
"npm-run-all": "^4.1.5",
"prettier": "^3.3.3",
"pseudolocale": "^2.1.0",
"rollup-plugin-modify": "^3.0.0",
"rollup-plugin-postcss-lit": "^2.1.0",
"storybook": "^8.3.4",
"storybook-addon-mock": "^5.0.0",
"syncpack": "^13.0.0",
"turnstile-types": "^1.2.3",
"typescript": "^5.6.2",
"typescript-eslint": "^8.8.0",
"vite-plugin-lit-css": "^2.0.0",
"vite-tsconfig-paths": "^5.0.1",
"wireit": "^0.14.9"
},
"engines": {
"node": ">=20"
},
"license": "MIT",
"optionalDependencies": {
"@esbuild/darwin-arm64": "^0.24.0",
"@esbuild/linux-amd64": "^0.18.11",
"@esbuild/linux-arm64": "^0.24.0",
"@rollup/rollup-darwin-arm64": "4.23.0",
"@rollup/rollup-linux-arm64-gnu": "4.23.0",
"@rollup/rollup-linux-x64-gnu": "4.23.0"
},
"overrides": {
"rapidoc": {
"@apitools/openapi-parser@": "0.0.37"
},
"chromedriver": {
"axios": "^1.8.4"
}
},
"prettier": "@goauthentik/prettier-config",
"private": true,
"scripts": {
"build": "wireit",
"build-locales": "wireit",
"build-locales:build": "wireit",
"build-proxy": "wireit",
"build:sfe": "wireit",
"build:sfe": "npm run build -w @goauthentik/web-sfe",
"esbuild:watch": "node scripts/build-web.mjs --watch",
"extract-locales": "wireit",
"format": "wireit",
"lint": "wireit",
"lint:imports": "wireit",
"lint": "eslint --fix .",
"lint-check": "eslint --max-warnings 0 .",
"lint:imports": "knip --config scripts/knip.config.ts",
"lint:lockfile": "wireit",
"lint:nightmare": "wireit",
"lint:package": "wireit",
"lint:precommit": "wireit",
"lint:types": "wireit",
"lit-analyse": "wireit",
"postinstall": "bash scripts/patch-spotlight.sh",
"precommit": "wireit",
"prettier": "wireit",
"prettier-check": "wireit",
"pseudolocalize": "wireit",
"prettier": "prettier --cache --write -u .",
"prettier-check": "prettier --cache --check -u .",
"pseudolocalize": "node ./scripts/pseudolocalize.mjs",
"storybook": "storybook dev -p 6006",
"storybook:build": "wireit",
"test": "wireit",
@ -172,6 +32,180 @@
"watch": "run-s build-locales esbuild:watch"
},
"type": "module",
"exports": {
"./package.json": "./package.json",
"./elements/*": "./src/elements/*",
"./common/*": "./src/common/*",
"./components/*": "./src/components/*",
"./flow/*": "./src/flow/*",
"./locales/*": "./src/locales/*",
"./user/*": "./src/user/*",
"./admin/*": "./src/admin/*",
"./*/browser": {
"types": "./out/*/browser.d.ts",
"import": "./*/browser.js"
},
"./*/node": {
"types": "./out/*/node.d.ts",
"import": "./*/node.js"
},
"./*": {
"types": "./out/*/index.d.ts",
"import": "./*/index.js"
}
},
"imports": {
"#common/*.css": "./src/common/*.css",
"#common/*": "./src/common/*.js",
"#elements/*.css": "./src/elements/*.css",
"#elements/*": "./src/elements/*.js",
"#components/*.css": "./src/components/*.css",
"#components/*": "./src/components/*.js",
"#user/*.css": "./src/user/*.css",
"#user/*": "./src/user/*.js",
"#admin/*.css": "./src/admin/*.css",
"#admin/*": "./src/admin/*.js",
"#flow/*.css": "./src/flow/*.css",
"#flow/*": "./src/flow/*.js",
"#locales/*": "./src/locales/*.js",
"#stories/*": "./src/stories/*.js",
"#*/browser": {
"types": "./out/*/browser.d.ts",
"import": "./*/browser.js"
},
"#*/node": {
"types": "./out/*/node.d.ts",
"import": "./*/node.js"
},
"#*": {
"types": "./out/*/index.d.ts",
"import": "./*/index.js"
}
},
"dependencies": {
"@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-html": "^6.4.9",
"@codemirror/lang-javascript": "^6.2.4",
"@codemirror/lang-python": "^6.1.6",
"@codemirror/lang-xml": "^6.1.0",
"@codemirror/legacy-modes": "^6.4.1",
"@codemirror/theme-one-dark": "^6.1.2",
"@floating-ui/dom": "^1.6.11",
"@formatjs/intl-listformat": "^7.7.11",
"@fortawesome/fontawesome-free": "^6.6.0",
"@goauthentik/api": "^2025.4.1-1747687715",
"@lit/context": "^1.1.2",
"@lit/localize": "^0.12.2",
"@lit/reactive-element": "^2.0.4",
"@lit/task": "^1.0.2",
"@mdx-js/mdx": "^3.1.0",
"@open-wc/lit-helpers": "^0.7.0",
"@patternfly/elements": "^4.1.0",
"@patternfly/patternfly": "^4.224.2",
"@sentry/browser": "^9.22.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",
"codemirror": "^6.0.1",
"construct-style-sheets-polyfill": "^3.1.0",
"core-js": "^3.38.1",
"country-flag-icons": "^1.5.19",
"date-fns": "^4.1.0",
"deepmerge-ts": "^7.1.5",
"dompurify": "^3.2.6",
"fuse.js": "^7.1.0",
"guacamole-common-js": "^1.5.0",
"hastscript": "^9.0.1",
"lit": "^3.2.0",
"md-front-matter": "^1.0.4",
"mermaid": "^11.4.1",
"rapidoc": "^9.3.8",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"rehype-highlight": "^7.0.2",
"rehype-mermaid": "^3.0.0",
"rehype-parse": "^9.0.1",
"rehype-stringify": "^10.0.1",
"remark-directive": "^4.0.0",
"remark-frontmatter": "^5.0.0",
"remark-gfm": "^4.0.1",
"remark-mdx-frontmatter": "^5.0.0",
"style-mod": "^4.1.2",
"trusted-types": "^2.0.0",
"ts-pattern": "^5.4.0",
"unist-util-visit": "^5.0.0",
"webcomponent-qr-code": "^1.2.0",
"yaml": "^2.8.0"
},
"devDependencies": {
"@eslint/js": "^9.27.0",
"@goauthentik/core": "^1.0.0",
"@goauthentik/esbuild-plugin-live-reload": "^1.0.4",
"@goauthentik/eslint-config": "^1.0.5",
"@goauthentik/prettier-config": "^1.0.5",
"@goauthentik/tsconfig": "^1.0.4",
"@hcaptcha/types": "^1.0.4",
"@lit/localize-tools": "^0.8.0",
"@storybook/addon-essentials": "^8.6.14",
"@storybook/addon-links": "^8.6.14",
"@storybook/blocks": "^8.6.12",
"@storybook/experimental-addon-test": "^8.6.14",
"@storybook/manager-api": "^8.6.14",
"@storybook/test": "^8.6.14",
"@storybook/web-components": "^8.6.14",
"@storybook/web-components-vite": "^8.6.14",
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
"@types/chart.js": "^2.9.41",
"@types/codemirror": "^5.60.15",
"@types/dompurify": "^3.0.5",
"@types/grecaptcha": "^3.0.9",
"@types/guacamole-common-js": "^1.5.3",
"@types/mocha": "^10.0.8",
"@types/node": "^22.15.21",
"@types/react": "^19.1.5",
"@types/react-dom": "^19.1.5",
"@typescript-eslint/eslint-plugin": "^8.8.0",
"@typescript-eslint/parser": "^8.8.0",
"@wdio/browser-runner": "9.4",
"@wdio/cli": "9.4",
"@wdio/spec-reporter": "^9.1.2",
"@web/test-runner": "^0.20.2",
"chromedriver": "^136.0.3",
"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.11.1",
"eslint-plugin-lit": "^2.1.1",
"eslint-plugin-wc": "^3.0.1",
"github-slugger": "^2.0.0",
"globals": "^15.10.0",
"knip": "^5.58.0",
"lit-analyzer": "^2.0.3",
"npm-run-all": "^4.1.5",
"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",
"typescript": "^5.8.3",
"typescript-eslint": "^8.32.1",
"vite-plugin-lit-css": "^2.0.0",
"vite-tsconfig-paths": "^5.0.1",
"wireit": "^0.14.12"
},
"optionalDependencies": {
"@esbuild/darwin-arm64": "^0.25.4",
"@esbuild/linux-arm64": "^0.25.4",
"@esbuild/linux-x64": "^0.25.4",
"@rollup/rollup-darwin-arm64": "^4.41.1",
"@rollup/rollup-linux-arm64-gnu": "^4.41.1",
"@rollup/rollup-linux-x64-gnu": "^4.41.1"
},
"wireit": {
"build": {
"#comment": [
@ -205,7 +239,7 @@
],
"dependencies": [
"build-locales",
"./packages/sfe:build"
"build:sfe"
],
"env": {
"NODE_RUNNER": {
@ -214,14 +248,6 @@
}
}
},
"build:sfe": {
"dependencies": [
"./packages/sfe:build"
],
"files": [
"./packages/sfe/**/*.ts"
]
},
"build-proxy": {
"command": "node scripts/build-web.mjs --proxy",
"dependencies": [
@ -247,34 +273,17 @@
"extract-locales": {
"command": "lit-localize extract"
},
"format": {
"command": "prettier --write .",
"dependencies": [
"lint:package"
]
},
"format:packages": {
"dependencies": [
"./packages/sfe:prettier"
]
},
"lint": {
"command": "eslint --max-warnings 0 --fix",
"env": {
"NODE_OPTIONS": "--max_old_space_size=65536"
}
},
"lint:components": {
"command": "lit-analyzer src"
},
"lint:imports": {
"command": "knip --config scripts/knip.config.ts"
},
"lint:types:tests": {
"command": "tsc --noEmit -p ./tests"
},
"lint:types": {
"command": "NODE_OPTIONS=\"--max-old-space-size=3000\" tsc -b .",
"command": "tsc -p .",
"env": {
"NODE_OPTIONS": "--max_old_space_size=8192"
},
"dependencies": [
"build-locales",
"lint:types:tests"
@ -285,62 +294,18 @@
"shell": true,
"command": "sh ./scripts/lint-lockfile.sh package-lock.json"
},
"lint:lockfiles": {
"dependencies": [
"./packages/sfe:lint:lockfile"
]
},
"lint:package": {
"command": "syncpack format -i ' '"
},
"lint:nightmare": {
"command": "${NODE_RUNNER} ./scripts/eslint.mjs --nightmare",
"env": {
"NODE_RUNNER": {
"external": true,
"default": "node"
}
}
},
"lint:precommit": {
"command": "${NODE_RUNNER} ./scripts/eslint.mjs --precommit",
"env": {
"NODE_RUNNER": {
"external": true,
"default": "node"
}
}
},
"lint:spelling": {
"command": "node scripts/check-spelling.mjs"
},
"lit-analyse": {
"command": "lit-analyzer src"
},
"precommit": {
"command": "prettier --write .",
"dependencies": [
"lint",
"lint:types",
"lint:components",
"lint:spelling",
"lint:package",
"lint:lockfile",
"lint:lockfiles",
"lint:precommit",
"format:packages"
"lint:lockfile"
]
},
"prettier": {
"dependencies": [
"format"
]
},
"prettier-check": {
"command": "prettier --check ."
},
"pseudolocalize": {
"command": "node scripts/pseudolocalize.mjs"
},
"storybook:build": {
"command": "storybook build",
"env": {
@ -383,13 +348,36 @@
}
},
"tsc": {
"command": "tsc -p .",
"env": {
"NODE_OPTIONS": "--max_old_space_size=8192"
},
"dependencies": [
"lint:types"
"build-locales"
]
}
},
"engines": {
"node": ">=20"
},
"workspaces": [
".",
"./packages/*"
]
],
"prettier": "@goauthentik/prettier-config",
"overrides": {
"@storybook/addon-docs": {
"react": "^19.1.0",
"react-dom": "^19.1.0"
},
"chromedriver": {
"axios": "^1.8.4"
},
"rapidoc": {
"@apitools/openapi-parser@": "0.0.37"
},
"storybook-addon-mock": {
"react": "^19.1.0",
"react-dom": "^19.1.0"
}
}
}

View File

@ -0,0 +1,18 @@
The MIT License (MIT)
Copyright (c) 2025 Authentik Security, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute,
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1,4 @@
# `@goauthentik/core`
This package contains utility scripts common to all TypeScript and JavaScript packages in the
`@goauthentik` monorepo.

View File

@ -0,0 +1,66 @@
/**
* @file Utility functions for working with environment variables.
*/
/// <reference types="../types/node.js" />
//#region Constants
/**
* The current Node.js environment, defaulting to "development" when not set.
*
* Note, this should only be used during the build process.
*
* If you need to check the environment at runtime, use `process.env.NODE_ENV` to
* ensure that module tree-shaking works correctly.
*
* @category Environment
* @runtime node
*/
export const NodeEnvironment = process.env.NODE_ENV || "development";
/**
* A source environment variable, which can be a string, number, boolean, null, or undefined.
* @typedef {string | number | boolean | null | undefined} EnvironmentVariable
*/
/**
* A type helper for serializing environment variables.
*
* @category Environment
* @template {EnvironmentVariable} T
* @typedef {T extends string ? `"${T}"` : T} JSONify
*/
//#endregion
//#region Utilities
/**
* Given an object of environment variables, serializes them into a mapping of
* environment variable names to their respective runtime constants.
*
* This is useful for defining environment variables while bundling with ESBuild, Vite, etc.
*
* @category Environment
* @runtime node
*
* @template {Record<string, EnvironmentVariable>} EnvRecord
* @template {string} [Prefix='import.meta.env.']
*
* @param {EnvRecord} input
* @param {Prefix} [prefix='import.meta.env.']
*
* @returns {{[K in keyof EnvRecord as `${Prefix}${K}`]: JSONify<EnvRecord[K]>}}
*/
export function serializeEnvironmentVars(
input,
prefix = /** @type {Prefix} */ ("import.meta.env."),
) {
const env = Object.fromEntries(
Object.entries(input).map(([key, value]) => [prefix + key, JSON.stringify(value ?? "")]),
);
return /** @type {any} */ (env);
}
//#endregion

View File

@ -0,0 +1,12 @@
/**
* @file Dummy entry point for the `core` package.
*
* This exists to make TypeScript's module resolution more predictable.
*
* @internal
* @ignore
*/
export {};
export default {};

View File

@ -0,0 +1,59 @@
{
"name": "@goauthentik/core",
"version": "1.0.0",
"description": "Core functionality for the authentik monorepo",
"license": "MIT",
"private": true,
"scripts": {
"build": "tsc -p .",
"prettier": "prettier --write .",
"prettier-check": "prettier --check ."
},
"main": "index.js",
"type": "module",
"exports": {
"./package.json": "./package.json",
"./*/browser": {
"types": "./out/*/browser.d.ts",
"import": "./*/browser.js"
},
"./*/node": {
"types": "./out/*/node.d.ts",
"import": "./*/node.js"
},
"./*": {
"types": "./out/*/index.d.ts",
"import": "./*/index.js"
},
".": {
"import": "./index.js",
"types": "./out/index.d.ts"
}
},
"imports": {
"#*/browser": {
"types": "./out/*/browser.d.ts",
"import": "./*/browser.js"
},
"#*/node": {
"types": "./out/*/node.d.ts",
"import": "./*/node.js"
},
"#*": {
"types": "./out/*/index.d.ts",
"import": "./*/index.js"
}
},
"devDependencies": {
"@goauthentik/prettier-config": "^1.0.5",
"@goauthentik/tsconfig": "^1.0.4",
"@types/node": "^22.15.21",
"prettier": "^3.3.3",
"typescript": "^5.6.3"
},
"engines": {
"node": ">=20.11"
},
"types": "./out/index.d.ts",
"prettier": "@goauthentik/prettier-config"
}

View File

@ -0,0 +1,49 @@
import { createRequire } from "node:module";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const relativeDirname = dirname(fileURLToPath(import.meta.url));
/**
* @typedef {'~authentik'} MonoRepoRoot
*/
/**
* The root of the authentik monorepo.
*
* @runtime node
*/
export const MonoRepoRoot = /** @type {MonoRepoRoot} */ (
resolve(relativeDirname, "..", "..", "..", "..")
);
/**
* Resolve a package name to its location in the monorepo to the single node_modules directory.
*
* @param {string} packageName
* @param {ImportMeta} [meta] The `import.meta` object of the module.
*
* @runtime node
* @returns {string} The resolved path to the package.
* @throws {Error} If the package cannot be resolved.
*/
export function resolvePackage(packageName, meta) {
const require = createRequire(meta ? meta.url : import.meta.url);
const relativePackageJSONPath = join(packageName, "package.json");
/** @type {string} */
let absolutePackageJSONPath;
try {
absolutePackageJSONPath = require.resolve(relativePackageJSONPath);
} catch (cause) {
const error = new Error(`🚫 Failed to resolve package "${packageName}"`);
error.cause = cause;
throw error;
}
return dirname(absolutePackageJSONPath);
}

View File

@ -0,0 +1,41 @@
import { createRequire } from "node:module";
import * as path from "node:path";
import * as process from "node:process";
import { fileURLToPath } from "node:url";
/**
* Predicate to determine if a module was run directly, i.e. not imported.
*
* @param {ImportMeta} meta The `import.meta` object of the module.
*
* @return {boolean} Whether the module was run directly.
* @runtime node
*/
export function isMain(meta) {
// Are we not in a module context?
if (!meta) return false;
const relativeScriptPath = process.argv[1];
if (!relativeScriptPath) return false;
const require = createRequire(meta.url);
const absoluteScriptPath = require.resolve(relativeScriptPath);
const modulePath = fileURLToPath(meta.url);
const scriptExtension = path.extname(absoluteScriptPath);
if (scriptExtension) {
return modulePath === absoluteScriptPath;
}
const moduleExtension = path.extname(modulePath);
if (moduleExtension) {
return absoluteScriptPath === modulePath.slice(0, -moduleExtension.length);
}
// If both are without extension, compare them directly.
return modulePath === absoluteScriptPath;
}

View File

@ -0,0 +1,11 @@
{
"extends": "@goauthentik/tsconfig",
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"resolveJsonModule": true,
"baseUrl": ".",
"checkJs": true,
"allowJs": true,
"emitDeclarationOnly": true
}
}

54
web/packages/core/types/node.d.ts vendored Normal file
View File

@ -0,0 +1,54 @@
/**
* @file Global variables provided by Node.js
*/
declare module "module" {
global {
/**
* @deprecated This is not present in ESM files.
*
* ```js
* import { dirname } from "node:path";
* import { fileURLToPath } from "node:url";
*
* const relativeDirname = dirname(fileURLToPath(import.meta.url));
* ```
*/
// eslint-disable-next-line no-var
var __dirname: string;
}
}
declare module "process" {
global {
namespace NodeJS {
interface ProcessEnv {
/**
* The port used by the Docker Compose HTTP service.
*/
COMPOSE_PORT_HTTP?: string;
/**
* The port used by the Docker Compose HTTPS service.
*/
COMPOSE_PORT_HTTPS?: string;
/**
* An environment variable used to determine
* whether Node.js is running in production mode.
*
* @see {@link https://nodejs.org/en/learn/getting-started/nodejs-the-difference-between-development-and-production | The difference between development and production}
* @runtime node
*/
readonly NODE_ENV?: "development" | "production";
/**
* @todo Determine where this is used and if it is needed,
* give it a better name.
* @deprecated
* @runtime node
*/
readonly AK_API_BASE_PATH?: string;
}
}
}
}

View File

@ -0,0 +1,21 @@
/**
* @file Utility functions for working with semantic versions.
*
* @runtime common
*/
/**
* Creates a URL to the release notes for the given version.
*
* @param {string} semver
* @returns {URL}
* @runtime common
*/
export function createReleaseNotesURL(semver) {
const segments = semver.split(".");
const versionFamily = segments.slice(0, -1).join(".");
const release = `${versionFamily}#fixed-in-${segments.join("")}`;
return new URL(`/docs/releases/${release}`, "https://goauthentik.io");
}

View File

@ -0,0 +1,54 @@
/**
* @file Utility functions for working with semantic versions.
*
* @runtime node
*/
import { MonoRepoRoot } from "#paths/node";
import { execSync } from "node:child_process";
import PackageJSON from "../../../../package.json" with { type: "json" };
/**
* The current version of authentik in SemVer format.
*
* @runtime node
*/
export const AuthentikVersion = /**@type {`${number}.${number}.${number}`} */ (PackageJSON.version);
/**
* Reads the last commit hash from the current git repository.
*
* @runtime node
*/
export function readGitBuildHash() {
try {
const commit = execSync("git rev-parse HEAD", {
encoding: "utf8",
cwd: MonoRepoRoot,
})
.toString()
.trim();
return commit;
} catch (_error) {
console.debug("Git commit could not be read.");
}
return process.env.GIT_BUILD_HASH || "";
}
/**
* Reads the build identifier for the current environment.
*
* This must match the behavior defined in authentik's server-side `get_full_version` function.
*
* @runtime node
* @see {@link "authentik\_\_init\_\_.py"}
*/
export function readBuildIdentifier() {
const { GIT_BUILD_HASH } = process.env;
if (!GIT_BUILD_HASH) return AuthentikVersion;
return [AuthentikVersion, GIT_BUILD_HASH].join("+");
}

View File

@ -0,0 +1,40 @@
# `@goauthentik/esbuild-plugin-live-reload`
_A plugin that enables live reloading of ESBuild during development._
## Usage
### Node.js setup
```js
import { liveReloadPlugin } from "@goauthentik/esbuild-plugin-live-reload";
import esbuild from "esbuild";
const NodeEnvironment = process.env.NODE_ENV || "development";
/**
* @type {esbuild.BuildOptions}
*/
const buildOptions = {
// ... Your build options.
define: {
"process.env.NODE_ENV": JSON.stringify(NodeEnvironment),
},
plugins: [liveReloadPlugin(/** @see {@link LiveReloadPluginOptions} */)],
};
const buildContext = await esbuild.context(buildOptions);
await buildContext.rebuild();
await buildContext.watch();
```
### Browser setup
```js
// Place this at the beginning of your application's entry point.
if (process.env.NODE_ENV === "development") {
await import("@goauthentik/esbuild-plugin-live-reload/client");
}
```

View File

@ -6,7 +6,7 @@
* @import { Message as ESBuildMessage } from "esbuild";
*/
const logPrefix = "👷 [ESBuild]";
const logPrefix = "authentik/dev/web: ";
const log = console.debug.bind(console, logPrefix);
/**
@ -76,7 +76,7 @@ export class ESBuildObserver extends EventSource {
*/
#startListener = () => {
this.#trackActivity();
log("⏰ Build started...");
log("⏰ Build started...");
};
#internalErrorListener = () => {
@ -86,7 +86,7 @@ export class ESBuildObserver extends EventSource {
clearTimeout(this.#keepAliveInterval);
this.close();
log("⛔️ Closing connection");
log("⛔️ Closing connection");
}
};
@ -126,13 +126,13 @@ export class ESBuildObserver extends EventSource {
this.#trackActivity();
if (!this.online) {
log("🚫 Build finished while offline.");
log("🚫 Build finished while offline.");
this.deferredReload = true;
return;
}
log("🛎️ Build completed! Reloading...");
log("🛎️ Build completed! Reloading...");
// We use an animation frame to keep the reload from happening before the
// event loop has a chance to process the message.
@ -189,13 +189,13 @@ export class ESBuildObserver extends EventSource {
if (!this.deferredReload) return;
log("🛎️ Reloading after offline build...");
log("🛎️ Reloading after offline build...");
this.deferredReload = false;
window.location.reload();
});
log("🛎️ Listening for build changes...");
log("🛎️ Listening for build changes...");
this.#keepAliveInterval = setInterval(() => {
const now = Date.now();
@ -203,7 +203,7 @@ export class ESBuildObserver extends EventSource {
if (now - this.lastUpdatedAt < 10_000) return;
this.alive = false;
log("👋 Waiting for build to start...");
log("👋 Waiting for build to start...");
}, 15_000);
}

View File

@ -4,15 +4,20 @@
export {};
declare global {
/**
* Environment variables injected by ESBuild.
*/
interface ImportMetaEnv {
/**
* The injected watcher URL for ESBuild.
* This is used for live reloading in development mode.
*
* @format url
*/
readonly ESBUILD_WATCHER_URL?: string;
}
interface ImportMeta {
readonly env: {
/**
* The injected watcher URL for ESBuild.
* This is used for live reloading in development mode.
*
* @format url
*/
ESBUILD_WATCHER_URL: string;
};
readonly env: ImportMetaEnv;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,22 +1,13 @@
{
"name": "@goauthentik/esbuild-plugin-live-reload",
"version": "1.0.5",
"description": "ESBuild plugin to watch for file changes and trigger client-side reloads.",
"version": "1.0.4",
"dependencies": {
"find-free-ports": "^3.1.1"
},
"devDependencies": {
"@goauthentik/prettier-config": "^1.0.4",
"@goauthentik/tsconfig": "^1.0.4",
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
"@types/node": "^22.14.1",
"esbuild": "^0.25.0",
"prettier": "^3.3.3",
"typescript": "^5.6.2"
},
"engines": {
"node": ">=20.11"
"license": "MIT",
"scripts": {
"build": "tsc -p ."
},
"main": "index.js",
"type": "module",
"exports": {
"./package.json": "./package.json",
".": {
@ -32,22 +23,34 @@
"import": "./plugin/index.js"
}
},
"dependencies": {
"find-free-ports": "^3.1.1"
},
"devDependencies": {
"@goauthentik/prettier-config": "^1.0.5",
"@goauthentik/tsconfig": "^1.0.4",
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
"@types/node": "^22.15.21",
"esbuild": "^0.25.4",
"prettier": "^3.5.3",
"prettier-plugin-packagejson": "^2.5.14",
"typescript": "^5.8.3"
},
"peerDependencies": {
"esbuild": "^0.25.4"
},
"engines": {
"node": ">=22"
},
"types": "./out/index.d.ts",
"files": [
"./index.js",
"client/**/*",
"plugin/**/*",
"out/**/*"
],
"license": "MIT",
"main": "index.js",
"peerDependencies": {
"esbuild": "^0.25.0"
},
"prettier": "@goauthentik/prettier-config",
"private": true,
"publishConfig": {
"access": "public"
},
"type": "module",
"types": "./out/index.d.ts"
}
}

View File

@ -67,7 +67,6 @@ async function findDisparatePort() {
* @category ESBuild
*/
export function createRequestHandler({ pathname, dispatcher, logPrefix = "Build Observer" }) {
// eslint-disable-next-line no-console
const log = console.log.bind(console, `[${logPrefix}]`);
/**
@ -130,19 +129,20 @@ export function createRequestHandler({ pathname, dispatcher, logPrefix = "Build
/**
* Options for the build observer plugin.
*
* @typedef {object} BuildObserverOptions
* @typedef {object} LiveReloadPluginOptions
*
* @property {HTTPServer | HTTPSServer} [server]
* @property {ListenOptions} [listenOptions]
* @property {string | URL} [publicURL]
* @property {string} [logPrefix]
* @property {string} [relativeRoot]
* @property {HTTPServer | HTTPSServer} [server] A server to listen on. If not provided, a new server will be created.
* @property {ListenOptions} [listenOptions] Options for the server's listen method.
* @property {string | URL} [publicURL] A URL to listen on. If not provided, a random port will be used.
* @property {string} [logPrefix] A prefix to use for log messages.
* @property {string} [relativeRoot] A relative path to the root of the project. This is used to resolve build errors, line numbers, and file paths.
*/
/**
* Creates a plugin that listens for build events and sends them to a server-sent event stream.
*
* @param {BuildObserverOptions} [options]
* @param {
* } [options]
* @returns {import('esbuild').Plugin}
*/
export function liveReloadPlugin(options = {}) {
@ -177,6 +177,10 @@ export function liveReloadPlugin(options = {}) {
"import.meta.env.ESBUILD_WATCHER_URL": JSON.stringify(publicURL.href),
};
build.initialOptions.define["process.env.NODE_ENV"] ??= JSON.stringify(
process.env.NODE_ENV || "development",
);
const requestHandler = createRequestHandler({
pathname: publicURL.pathname,
dispatcher,
@ -191,7 +195,6 @@ export function liveReloadPlugin(options = {}) {
};
server.listen(listenOptions, () => {
// eslint-disable-next-line no-console
console.log(`[${logPrefix}] Listening`);
});
@ -241,3 +244,5 @@ export function liveReloadPlugin(options = {}) {
},
};
}
export default liveReloadPlugin;

View File

@ -1,23 +0,0 @@
{
"arrowParens": "always",
"bracketSpacing": true,
"embeddedLanguageFormatting": "auto",
"htmlWhitespaceSensitivity": "css",
"insertPragma": false,
"jsxSingleQuote": false,
"printWidth": 100,
"proseWrap": "preserve",
"quoteProps": "consistent",
"requirePragma": false,
"semi": true,
"singleQuote": false,
"tabWidth": 4,
"trailingComma": "all",
"useTabs": false,
"vueIndentScriptAndStyle": false,
"plugins": ["@trivago/prettier-plugin-sort-imports"],
"importOrder": ["^(@?)lit(.*)$", "\\.css$", "^@goauthentik/api$", "^[./]"],
"importOrderSeparation": true,
"importOrderSortSpecifiers": true,
"importOrderParserPlugins": ["typescript", "classProperties", "decorators-legacy"]
}

View File

@ -1,6 +1,14 @@
{
"name": "@goauthentik/web-sfe",
"version": "0.0.0",
"license": "MIT",
"private": true,
"scripts": {
"build": "rollup -c rollup.config.mjs --bundleConfigAsCjs",
"prettier": "prettier --cache --write -u .",
"prettier-check": "prettier --cache --check -u .",
"watch": "rollup -w -c rollup.config.mjs --bundleConfigAsCjs"
},
"dependencies": {
"@goauthentik/api": "^2024.6.0-1719577139",
"base64-js": "^1.5.1",
@ -10,20 +18,17 @@
"weakmap-polyfill": "^2.0.4"
},
"devDependencies": {
"@goauthentik/core": "^1.0.0",
"@rollup/plugin-commonjs": "^28.0.0",
"@rollup/plugin-node-resolve": "^15.3.0",
"@rollup/plugin-node-resolve": "^16.0.1",
"@rollup/plugin-swc": "^0.4.0",
"@swc/cli": "^0.4.0",
"@swc/cli": "^0.7.7",
"@swc/core": "^1.7.28",
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@types/jquery": "^3.5.31",
"lockfile-lint": "^4.14.0",
"prettier": "^3.3.2",
"rollup": "^4.23.0",
"rollup-plugin-copy": "^3.5.0",
"wireit": "^0.14.9"
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
"prettier": "^3.5.3",
"rollup": "^4.41.1",
"rollup-plugin-copy": "^3.5.0"
},
"license": "MIT",
"optionalDependencies": {
"@swc/core": "^1.7.28",
"@swc/core-darwin-arm64": "^1.6.13",
@ -36,33 +41,5 @@
"@swc/core-win32-arm64-msvc": "^1.6.13",
"@swc/core-win32-ia32-msvc": "^1.6.13",
"@swc/core-win32-x64-msvc": "^1.6.13"
},
"private": true,
"scripts": {
"build": "wireit",
"lint:lockfile": "wireit",
"prettier": "prettier --write ./src ./tsconfig.json ./rollup.config.js ./package.json",
"watch": "rollup -w -c rollup.config.js --bundleConfigAsCjs"
},
"wireit": {
"build:sfe": {
"command": "rollup -c rollup.config.js --bundleConfigAsCjs",
"files": [
"../../node_modules/bootstrap/dist/css/bootstrap.min.css",
"src/index.ts"
],
"output": [
"./dist/sfe/*"
]
},
"build": {
"command": "mkdir -p ../../dist/sfe && cp -r dist/sfe/* ../../dist/sfe",
"dependencies": [
"build:sfe"
]
},
"lint:lockfile": {
"command": "lockfile-lint --path package.json --type npm --allowed-hosts npm --validate-https"
}
}
}

View File

@ -1,43 +0,0 @@
import commonjs from "@rollup/plugin-commonjs";
import resolve from "@rollup/plugin-node-resolve";
import swc from "@rollup/plugin-swc";
import copy from "rollup-plugin-copy";
export default {
input: "src/index.ts",
output: {
dir: "./dist/sfe",
format: "cjs",
},
context: "window",
plugins: [
copy({
targets: [
{
src: "../../node_modules/bootstrap/dist/css/bootstrap.min.css",
dest: "./dist/sfe",
},
],
}),
resolve({ browser: true }),
commonjs(),
swc({
swc: {
jsc: {
loose: false,
externalHelpers: false,
// Requires v1.2.50 or upper and requires target to be es2016 or upper.
keepClassNames: false,
},
minify: false,
env: {
targets: {
edge: "17",
ie: "11",
},
mode: "entry",
},
},
}),
],
};

View File

@ -0,0 +1,61 @@
/**
* @file Rollup configuration for the SFE package.
*/
import commonjs from "@rollup/plugin-commonjs";
import resolve from "@rollup/plugin-node-resolve";
import swc from "@rollup/plugin-swc";
import { resolve as resolvePath } from "node:path";
import copy from "rollup-plugin-copy";
export async function createConfig() {
// TODO: Move this to a synchronous import once the repo root has NPM Workspaces.
const { resolvePackage, MonoRepoRoot } = await import("@goauthentik/core/paths/node");
const distDirectory = resolvePath(MonoRepoRoot, "web", "dist", "sfe");
const bootstrapDirectory = resolvePackage("bootstrap", import.meta);
const config = {
input: "src/index.ts",
output: {
dir: distDirectory,
format: "cjs",
},
context: "window",
plugins: [
copy({
targets: [
{
src: resolvePath(bootstrapDirectory, "dist", "css", "bootstrap.min.css"),
dest: distDirectory,
},
],
}),
resolve({ browser: true }),
commonjs(),
swc({
swc: {
jsc: {
loose: false,
externalHelpers: false,
// Requires v1.2.50 or upper and requires target to be es2016 or upper.
keepClassNames: false,
},
minify: false,
env: {
targets: {
edge: "17",
ie: "11",
},
mode: "entry",
},
},
}),
],
};
return config;
}
console.log("Preparing SFE package...");
export default createConfig;

View File

@ -47,7 +47,16 @@ class SimpleFlowExecutor {
return `${ak().api.base}api/v3/flows/executor/${this.flowSlug}/?query=${encodeURIComponent(window.location.search.substring(1))}`;
}
loading() {
this.container.innerHTML = `<div class="d-flex justify-content-center">
<div class="spinner-border spinner-border-md" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>`;
}
start() {
this.loading();
$.ajax({
type: "GET",
url: this.apiURL,
@ -201,6 +210,9 @@ class PasswordStage extends Stage<PasswordChallenge> {
<form id="password-form">
<img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
<h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1>
<div class="form-label-group my-3">
<input type="text" readonly class="form-control-plaintext" value="Welcome, ${this.challenge?.pendingUser}.">
</div>
<div class="form-label-group my-3 has-validation">
<input type="password" autofocus class="form-control ${this.error("password").length > 0 ? IS_INVALID : ""}" name="password" placeholder="Password">
${this.renderInputError("password")}

View File

@ -1,6 +1,9 @@
{
"compilerOptions": {
"types": ["jquery"],
"noEmit": true,
"baseUrl": ".",
"outDir": "out",
"types": [],
"esModuleInterop": true,
"lib": ["DOM", "ES2015", "ES2017"]
}

42
web/packages/sfe/types/jquery.d.ts vendored Normal file
View File

@ -0,0 +1,42 @@
/**
* @file Minimal typings for jQuery.
*
* This file provides a small subset of the jQuery typings used in authentik's
* Simplified Flow Executor.
*
* Unlike the `@types/jquery` package, this package does not expose `$` as a global
* which causes issues with WebdriverIO.
*/
declare module "jquery" {
export function JQuery<TElement = HTMLElement>(selector: string): JQuery<TElement>;
export namespace JQuery {
function ajax(options: JQueryAjaxSettings): void;
}
export interface JQuery<TElement = HTMLElement> extends ArrayLike<TElement> {
getElements(): TElement[];
getElement(): TElement;
getText(): string;
submit(): void;
html(html: string): JQuery<TElement>;
addClass(className: string): JQuery<TElement>;
removeClass(className: string): JQuery<TElement>;
on(eventName: string, handler: (event: Event) => void): JQuery<TElement>;
trigger(eventName: string): void;
ajax(options: JQueryAjaxSettings): void;
}
export interface JQueryAjaxSettings {
url: string;
type?: string;
data?: string;
contentType?: string;
dataType?: string;
success?: (data: unknown) => void;
error?: (data: unknown) => void;
}
export default JQuery;
}

19
web/paths/index.js Normal file
View File

@ -0,0 +1,19 @@
/**
* @file Paths used by the web package.
*
* @runtime common
*/
/**
* The name of the distribution directory.
*
* @runtime common
*/
export const DistDirectoryName = "dist";
/**
* The name of the static file directory.
*
* @runtime common
*/
export const StaticDirectoryName = "static";

85
web/paths/node.js Normal file
View File

@ -0,0 +1,85 @@
/**
* @file Paths used by the web package.
*
* @runtime node
*/
import { DistDirectoryName } from "#paths";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const relativeDirname = dirname(fileURLToPath(import.meta.url));
//#region Base paths
/**
* @typedef {'@goauthentik/web'} WebPackageIdentifier
*/
/**
* The root of the web package.
*
* @runtime node
*/
export const PackageRoot = /** @type {WebPackageIdentifier} */ (resolve(relativeDirname, ".."));
/**
* Path to the web package's distribution directory.
*
* This is where the built files are located after running the build process.
*
* @runtime node
*/
export const DistDirectory = /** @type {`${WebPackageIdentifier}/${DistDirectoryName}`} */ (
resolve(PackageRoot, DistDirectoryName)
);
//#endregion
//#region Entry points
/**
* @typedef {{ in: string, out: string }} EntryPointTarget
*
* ESBuild entrypoint target.
* Matches the type defined in the ESBuild context.
*/
/**
* Entry points available for building.
*
* @satisfies {Record<string, EntryPointTarget>}
*
* @runtime node
*/
export const EntryPoint = /** @type {const} */ ({
Admin: {
in: resolve(PackageRoot, "src", "admin", "AdminInterface", "index.entrypoint.ts"),
out: resolve(DistDirectory, "admin", "AdminInterface"),
},
User: {
in: resolve(PackageRoot, "src", "user", "index.entrypoint.ts"),
out: resolve(DistDirectory, "user", "UserInterface"),
},
Flow: {
in: resolve(PackageRoot, "src", "flow", "index.entrypoint.ts"),
out: resolve(DistDirectory, "flow", "FlowInterface"),
},
Standalone: {
in: resolve(PackageRoot, "src", "standalone", "api-browser/index.entrypoint.ts"),
out: resolve(DistDirectory, "standalone", "api-browser", "index"),
},
StandaloneLoading: {
in: resolve(PackageRoot, "src", "standalone", "loading/index.entrypoint.ts"),
out: resolve(DistDirectory, "standalone", "loading", "index"),
},
RAC: {
in: resolve(PackageRoot, "src", "rac", "index.entrypoint.ts"),
out: resolve(DistDirectory, "rac", "index"),
},
Polyfill: {
in: resolve(PackageRoot, "src", "polyfill", "index.entrypoint.ts"),
out: resolve(DistDirectory, "poly"),
},
});
//#endregion

View File

@ -1,17 +1,34 @@
import { spawnSync } from "child_process";
import fs from "fs";
import path from "path";
import process from "process";
/**
* @file Lit Localize build script.
*
* @remarks
* Determines if all the Xliff translation source files are present and
* if the Typescript source files generated from those sources are up-to-date.
*
* If they are not, it runs the locale building script, intercepting the
* long spew of "this string is not translated" and replacing it with a
* summary of how many strings are missing with respect to the source locale.
*
* @import { ConfigFile } from "@lit/localize-tools/lib/types/config"
* @import { Stats } from "fs";
*/
import { PackageRoot } from "#paths/node";
import { spawnSync } from "node:child_process";
import { readFileSync, statSync } from "node:fs";
import path from "node:path";
/**
* Determines if all the Xliff translation source files are present and if the Typescript source
* files generated from those sources are up-to-date. If they are not, it runs the locale building
* script, intercepting the long spew of "this string is not translated" and replacing it with a
* summary of how many strings are missing with respect to the source locale.
* @type {ConfigFile}
*/
const localizeRules = JSON.parse(
readFileSync(path.join(PackageRoot, "lit-localize.json"), "utf-8"),
);
const localizeRules = JSON.parse(fs.readFileSync("./lit-localize.json", "utf-8"));
/**
*
* @param {string} loc
* @returns {boolean}
*/
function generatedFileIsUpToDateWithXliffSource(loc) {
const xliff = path.join("./xliff", `${loc}.xlf`);
const gened = path.join("./src/locales", `${loc}.ts`);
@ -21,16 +38,26 @@ function generatedFileIsUpToDateWithXliffSource(loc) {
// than the generated file. The missing XLF file is important enough it
// generates a unique error message and halts the build.
/**
* @type {Stats}
*/
let xlfStat;
try {
var xlfStat = fs.statSync(xliff);
xlfStat = statSync(xliff);
} catch (_error) {
console.error(`lit-localize expected '${loc}.xlf', but XLF file is not present`);
process.exit(1);
}
/**
* @type {Stats}
*/
let genedStat;
// If the generated file doesn't exist, of course it's not up to date.
try {
var genedStat = fs.statSync(gened);
genedStat = statSync(gened);
} catch (_error) {
return false;
}

View File

@ -1,144 +1,83 @@
/// <reference types="../types/esbuild.js" />
/**
* @file ESBuild script for building the authentik web UI.
*
* @import { BuildOptions } from "esbuild";
*/
import { liveReloadPlugin } from "@goauthentik/esbuild-plugin-live-reload/plugin";
import { execFileSync } from "child_process";
import { mdxPlugin } from "#bundler/mdx-plugin/node";
import { createBundleDefinitions } from "#bundler/utils/node";
import { DistDirectoryName } from "#paths";
import { DistDirectory, EntryPoint, PackageRoot } from "#paths/node";
import { NodeEnvironment } from "@goauthentik/core/environment/node";
import { MonoRepoRoot, resolvePackage } from "@goauthentik/core/paths/node";
import { readBuildIdentifier } from "@goauthentik/core/version/node";
import { deepmerge } from "deepmerge-ts";
import esbuild from "esbuild";
import copy from "esbuild-plugin-copy";
import { polyfillNode } from "esbuild-plugin-polyfill-node";
import { copyFileSync, mkdirSync, readFileSync, statSync } from "fs";
import { globSync } from "glob";
import * as path from "path";
import { cwd } from "process";
import process from "process";
import { fileURLToPath } from "url";
import * as fs from "node:fs/promises";
import * as path from "node:path";
import { mdxPlugin } from "./esbuild/build-mdx-plugin.mjs";
const logPrefix = "[Build]";
const __dirname = fileURLToPath(new URL(".", import.meta.url));
let authentikProjectRoot = path.join(__dirname, "..", "..");
try {
// Use the package.json file in the root folder, as it has the current version information.
authentikProjectRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], {
encoding: "utf8",
}).replace("\n", "");
} catch (_error) {
// We probably don't have a .git folder, which could happen in container builds.
}
const packageJSONPath = path.join(authentikProjectRoot, "./package.json");
const rootPackage = JSON.parse(readFileSync(packageJSONPath, "utf8"));
const NODE_ENV = process.env.NODE_ENV || "development";
const AK_API_BASE_PATH = process.env.AK_API_BASE_PATH || "";
const environmentVars = new Map([
["NODE_ENV", NODE_ENV],
["CWD", cwd()],
["AK_API_BASE_PATH", AK_API_BASE_PATH],
]);
const definitions = Object.fromEntries(
Array.from(environmentVars).map(([key, value]) => {
return [`process.env.${key}`, JSON.stringify(value)];
}),
);
const patternflyPath = resolvePackage("@patternfly/patternfly", import.meta);
/**
* All is magic is just to make sure the assets are copied into the right places. This is a very
* stripped down version of what the rollup-copy-plugin does, without any of the features we don't
* use, and using globSync instead of globby since we already had globSync lying around thanks to
* Typescript. If there's a third argument in an array entry, it's used to replace the internal path
* before concatenating it all together as the destination target.
* @type {Array<[string, string, string?]>}
*/
const assetsFileMappings = [
["node_modules/@patternfly/patternfly/patternfly.min.css", "."],
["node_modules/@patternfly/patternfly/assets/**", ".", "node_modules/@patternfly/patternfly/"],
["src/common/styles/**", "."],
["src/assets/images/**", "./assets/images"],
["./icons/*", "./assets/icons"],
];
/**
* @param {string} filePath
*/
const isFile = (filePath) => statSync(filePath).isFile();
/**
* @param {string} src Source file
* @param {string} dest Destination folder
* @param {string} [strip] Path to strip from the source file
*/
function nameCopyTarget(src, dest, strip) {
const target = path.join(dest, strip ? src.replace(strip, "") : path.parse(src).base);
return [src, target];
}
for (const [source, rawdest, strip] of assetsFileMappings) {
const matchedPaths = globSync(source);
const dest = path.join("dist", rawdest);
const copyTargets = matchedPaths.map((path) => nameCopyTarget(path, dest, strip));
for (const [src, dest] of copyTargets) {
if (isFile(src)) {
mkdirSync(path.dirname(dest), { recursive: true });
copyFileSync(src, dest);
}
}
}
/**
* @typedef {[source: string, destination: string]} EntryPoint
*/
/**
* This starts the definitions used for esbuild: Our targets, our arguments, the function for
* running a build, and three options for building: watching, building, and building the proxy.
* Ordered by largest to smallest interface to build even faster
*
* @type {EntryPoint[]}
*/
const entryPoints = [
["admin/AdminInterface/AdminInterface.ts", "admin"],
["user/UserInterface.ts", "user"],
["flow/FlowInterface.ts", "flow"],
["standalone/api-browser/index.ts", "standalone/api-browser"],
["rac/index.ts", "rac"],
["standalone/loading/index.ts", "standalone/loading"],
["polyfill/poly.ts", "."],
];
/**
* @type {import("esbuild").BuildOptions}
* @type {Readonly<BuildOptions>}
*/
const BASE_ESBUILD_OPTIONS = {
entryNames: `[dir]/[name]-${readBuildIdentifier()}`,
chunkNames: "[dir]/chunks/[hash]",
assetNames: "assets/[dir]/[name]-[hash]",
publicPath: path.join("/static", DistDirectoryName),
outdir: DistDirectory,
bundle: true,
write: true,
sourcemap: true,
minify: NODE_ENV === "production",
minify: NodeEnvironment === "production",
legalComments: "external",
splitting: true,
treeShaking: true,
external: ["*.woff", "*.woff2"],
tsconfig: path.resolve(__dirname, "..", "tsconfig.build.json"),
tsconfig: path.resolve(PackageRoot, "tsconfig.build.json"),
loader: {
".css": "text",
},
plugins: [
copy({
assets: [
{
from: path.join(patternflyPath, "patternfly.min.css"),
to: ".",
},
{
from: path.join(patternflyPath, "assets", "**"),
to: "./assets",
},
{
from: path.resolve(PackageRoot, "src", "common", "styles", "**"),
to: ".",
},
{
from: path.resolve(PackageRoot, "src", "assets", "images", "**"),
to: "./assets/images",
},
{
from: path.resolve(PackageRoot, "icons", "*"),
to: "./assets/icons",
},
],
}),
polyfillNode({
polyfills: {
path: true,
},
}),
mdxPlugin({
root: authentikProjectRoot,
root: MonoRepoRoot,
}),
],
define: definitions,
define: createBundleDefinitions(),
format: "esm",
logOverride: {
/**
@ -151,69 +90,43 @@ const BASE_ESBUILD_OPTIONS = {
},
};
/**
* Creates a version ID for the build.
* @returns {string}
*/
function composeVersionID() {
const { version } = rootPackage;
const buildHash = process.env.GIT_BUILD_HASH;
async function cleanDistDirectory() {
const timerLabel = `${logPrefix} ♻️ Cleaning previous builds...`;
if (buildHash) {
return `${version}+${buildHash}`;
}
console.time(timerLabel);
return version;
await fs.rm(DistDirectory, {
recursive: true,
force: true,
});
await fs.mkdir(DistDirectory, {
recursive: true,
});
console.timeEnd(timerLabel);
}
/**
* Build a single entry point.
* Creates an ESBuild options, extending the base options with the given overrides.
*
* @param {EntryPoint} buildTarget
* @param {Partial<esbuild.BuildOptions>} [overrides]
* @throws {Error} on build failure
* @param {BuildOptions} overrides
* @returns {BuildOptions}
*/
function createEntryPointOptions([source, dest], overrides = {}) {
const outdir = path.join(__dirname, "..", "dist", dest);
export function createESBuildOptions(overrides) {
/**
* @type {esbuild.BuildOptions}
* @type {BuildOptions}
*/
const mergedOptions = deepmerge(BASE_ESBUILD_OPTIONS, overrides);
const entryPointConfig = {
entryPoints: [`./src/${source}`],
entryNames: `[dir]/[name]-${composeVersionID()}`,
publicPath: path.join("/static", "dist", dest),
outdir,
};
/**
* @type {esbuild.BuildOptions}
*/
const mergedConfig = deepmerge(BASE_ESBUILD_OPTIONS, entryPointConfig, overrides);
return mergedConfig;
}
/**
* Build all entry points in parallel.
*
* @param {EntryPoint[]} entryPoints
* @returns {Promise<esbuild.BuildResult[]>}
*/
async function buildParallel(entryPoints) {
return Promise.all(
entryPoints.map((entryPoint) => {
return esbuild.build(createEntryPointOptions(entryPoint));
}),
);
return mergedOptions;
}
function doHelp() {
console.log(`Build the authentik UI
options:
-w, --watch: Build all ${entryPoints.length} interfaces
-w, --watch: Build all interfaces
-p, --proxy: Build only the polyfills and the loading application
-h, --help: This help message
`);
@ -222,27 +135,46 @@ function doHelp() {
}
async function doWatch() {
console.log("Watching all entry points...");
console.group(`${logPrefix} 🤖 Watching entry points`);
const buildContexts = await Promise.all(
entryPoints.map((entryPoint) => {
return esbuild.context(
createEntryPointOptions(entryPoint, {
define: definitions,
plugins: [
liveReloadPlugin({
logPrefix: `Build Observer (${entryPoint[1]})`,
relativeRoot: path.join(__dirname, ".."),
}),
],
}),
);
}),
);
const entryPoints = Object.entries(EntryPoint).map(([entrypointID, target]) => {
console.log(entrypointID);
await Promise.all(buildContexts.map((context) => context.rebuild()));
return target;
});
await Promise.allSettled(buildContexts.map((context) => context.watch()));
console.groupEnd();
const developmentPlugins = await import("@goauthentik/esbuild-plugin-live-reload/plugin")
.then(({ liveReloadPlugin }) => [
liveReloadPlugin({
relativeRoot: PackageRoot,
}),
])
.catch(() => []);
const buildOptions = createESBuildOptions({
entryPoints,
plugins: developmentPlugins,
});
const buildContext = await esbuild.context(buildOptions);
await buildContext.rebuild();
await buildContext.watch();
const httpURL = new URL("http://localhost");
httpURL.port = process.env.COMPOSE_PORT_HTTP ?? "9000";
const httpsURL = new URL("https://localhost");
httpsURL.port = process.env.COMPOSE_PORT_HTTPS ?? "9443";
console.log(`\n${logPrefix} 🚀 Server running\n\n`);
console.log(` 🔓 ${httpURL.href}`);
console.log(` 🔒 ${httpsURL.href}`);
console.log(`\n---`);
return /** @type {Promise<void>} */ (
new Promise((resolve) => {
@ -254,15 +186,34 @@ async function doWatch() {
}
async function doBuild() {
console.log("Building all entry points");
console.group(`${logPrefix} 🚀 Building entry points:`);
return buildParallel(entryPoints);
const entryPoints = Object.entries(EntryPoint).map(([entrypointID, target]) => {
console.log(entrypointID);
return target;
});
console.groupEnd();
const buildOptions = createESBuildOptions({
entryPoints,
});
await esbuild.build(buildOptions);
console.log("Build complete");
}
async function doProxy() {
return buildParallel(
entryPoints.filter(([_, dest]) => ["standalone/loading", "."].includes(dest)),
);
const entryPoints = [EntryPoint.StandaloneLoading];
const buildOptions = createESBuildOptions({
entryPoints,
});
await esbuild.build(buildOptions);
console.log("Proxy build complete");
}
async function delegateCommand() {
@ -284,12 +235,15 @@ async function delegateCommand() {
}
}
await delegateCommand()
.then(() => {
console.log("Build complete");
process.exit(0);
})
.catch((error) => {
console.error(error);
process.exit(1);
});
await cleanDistDirectory()
// ---
.then(() =>
delegateCommand()
.then(() => {
process.exit(0);
})
.catch((error) => {
console.error(error);
process.exit(1);
}),
);

View File

@ -1,15 +0,0 @@
import { execSync } from "child_process";
import path from "path";
const projectRoot = execSync("git rev-parse --show-toplevel", { encoding: "utf8" }).replace(
"\n",
"",
);
const cmd = [
"codespell -D -",
`-D ${path.join(projectRoot, ".github/codespell-dictionary.txt")}`,
`-I ${path.join(projectRoot, ".github/codespell-words.txt")}`,
"-S './src/locales/**' ./src -s",
].join(" ");
console.log(execSync(cmd, { encoding: "utf8" }));

View File

@ -1,81 +0,0 @@
/**
* @import {
OnLoadArgs,
OnLoadResult,
Plugin,
PluginBuild
* } from 'esbuild'
*/
import * as fs from "node:fs/promises";
import * as path from "node:path";
/**
* @typedef {Omit<OnLoadArgs, 'pluginData'> & LoadDataFields} LoadData
* Data passed to `onload`.
*
* @typedef LoadDataFields
* Extra fields given in `data` to `onload`.
* @property {PluginData | null | undefined} [pluginData]
* Plugin data.
*
*
* @typedef PluginData
* Extra data passed.
* @property {Buffer | string | null | undefined} [contents]
* File contents.
*/
const name = "mdx-plugin";
/**
* @typedef MDXPluginOptions
*
* @property {string} root Root directory.
*/
/**
* Bundle MDX into JSON modules.
*
* @param {MDXPluginOptions} options Options.
* @returns {Plugin} Plugin.
*/
export function mdxPlugin({ root }) {
return { name, setup };
/**
* @param {PluginBuild} build
* Build.
* @returns {undefined}
* Nothing.
*/
function setup(build) {
build.onLoad({ filter: /\.mdx?$/ }, onload);
/**
* @param {LoadData} data
* Data.
* @returns {Promise<OnLoadResult>}
* Result.
*/
async function onload(data) {
const content = String(
data.pluginData &&
data.pluginData.contents !== null &&
data.pluginData.contents !== undefined
? data.pluginData.contents
: await fs.readFile(data.path),
);
const publicPath = path.resolve(
"/",
path.relative(path.join(root, "website"), data.path),
);
const publicDirectory = path.dirname(publicPath);
return {
contents: JSON.stringify({ content, publicPath, publicDirectory }),
loader: "file",
};
}
}
}

View File

@ -1,56 +0,0 @@
import { execFileSync } from "child_process";
import { ESLint } from "eslint";
import fs from "fs";
import path from "path";
import process from "process";
import { fileURLToPath } from "url";
function changedFiles() {
const gitStatus = execFileSync("git", ["diff", "--name-only", "HEAD"], { encoding: "utf8" });
const gitUntracked = execFileSync("git", ["ls-files", "--others", "--exclude-standard"], {
encoding: "utf8",
});
const changed = gitStatus
.split("\n")
.filter((line) => line.trim().substring(0, 4) === "web/")
.filter((line) => /\.(m|c)?(t|j)s$/.test(line))
.map((line) => line.substring(4))
.filter((line) => fs.existsSync(line));
const untracked = gitUntracked
.split("\n")
.filter((line) => /\.(m|c)?(t|j)s$/.test(line))
.filter((line) => fs.existsSync(line));
const sourceFiles = [...changed, ...untracked].filter((line) => /^src\//.test(line));
const scriptFiles = [...changed, ...untracked].filter(
(line) => /^scripts\//.test(line) || !/^src\//.test(line),
);
return [...sourceFiles, ...scriptFiles];
}
const __dirname = fileURLToPath(new URL(".", import.meta.url));
const projectRoot = path.join(__dirname, "..");
process.chdir(projectRoot);
const hasFlag = (flags) => process.argv.length > 1 && flags.includes(process.argv[2]);
const [configFile, files] = hasFlag(["-n", "--nightmare"])
? [path.join(__dirname, "eslint.nightmare.mjs"), changedFiles()]
: hasFlag(["-p", "--precommit"])
? [path.join(__dirname, "eslint.precommit.mjs"), changedFiles()]
: [path.join(projectRoot, "eslint.config.mjs"), ["."]];
const eslint = new ESLint({
overrideConfigFile: configFile,
warnIgnored: false,
});
const results = await eslint.lintFiles(files);
const formatter = await eslint.loadFormatter("stylish");
const resultText = formatter.format(results);
const errors = results.reduce((acc, result) => acc + result.errorCount, 0);
console.log(resultText);
process.exit(errors > 1 ? 1 : 0);

View File

@ -1,217 +0,0 @@
import eslint from "@eslint/js";
import tsparser from "@typescript-eslint/parser";
import litconf from "eslint-plugin-lit";
import wcconf from "eslint-plugin-wc";
import globals from "globals";
import tseslint from "typescript-eslint";
const MAX_DEPTH = 4;
const MAX_NESTED_CALLBACKS = 4;
const MAX_PARAMS = 5;
// Waiting for SonarJS to be compatible
// const MAX_COGNITIVE_COMPLEXITY = 9;
const rules = {
"accessor-pairs": "error",
"array-callback-return": "error",
"block-scoped-var": "error",
"consistent-return": "error",
"consistent-this": ["error", "that"],
"curly": ["error", "all"],
"dot-notation": [
"error",
{
allowKeywords: true,
},
],
"eqeqeq": "error",
"func-names": "error",
"guard-for-in": "error",
"max-depth": ["error", MAX_DEPTH],
"max-nested-callbacks": ["error", MAX_NESTED_CALLBACKS],
"max-params": ["error", MAX_PARAMS],
"new-cap": "error",
"no-alert": "error",
"no-array-constructor": "error",
"no-bitwise": "error",
"no-caller": "error",
"no-case-declarations": "error",
"no-class-assign": "error",
"no-cond-assign": "error",
"no-const-assign": "error",
"no-constant-condition": "error",
"no-control-regex": "error",
"no-debugger": "error",
"no-delete-var": "error",
"no-div-regex": "error",
"no-dupe-args": "error",
"no-dupe-keys": "error",
"no-duplicate-case": "error",
"no-else-return": "error",
"no-empty": "error",
"no-empty-character-class": "error",
"no-empty-function": "error",
"no-labels": "error",
"no-eq-null": "error",
"no-eval": "error",
"no-ex-assign": "error",
"no-extend-native": "error",
"no-extra-bind": "error",
"no-extra-boolean-cast": "error",
"no-extra-label": "error",
"no-fallthrough": "error",
"no-func-assign": "error",
"no-implied-eval": "error",
"no-implicit-coercion": "error",
"no-implicit-globals": "error",
"no-inner-declarations": ["error", "functions"],
"no-invalid-regexp": "error",
"no-irregular-whitespace": "error",
"no-iterator": "error",
"no-invalid-this": "error",
"no-label-var": "error",
"no-lone-blocks": "error",
"no-lonely-if": "error",
"no-loop-func": "error",
"no-magic-numbers": ["error", { ignore: [0, 1, -1] }],
"no-multi-str": "error",
"no-negated-condition": "error",
"no-nested-ternary": "error",
"no-new": "error",
"no-new-func": "error",
"no-new-wrappers": "error",
"no-obj-calls": "error",
"no-octal": "error",
"no-octal-escape": "error",
"no-param-reassign": "error",
"no-proto": "error",
"no-redeclare": "error",
"no-regex-spaces": "error",
"no-restricted-syntax": ["error", "WithStatement"],
"no-script-url": "error",
"no-self-assign": "error",
"no-self-compare": "error",
"no-sequences": "error",
"no-shadow": "error",
"no-shadow-restricted-names": "error",
"no-sparse-arrays": "error",
"no-this-before-super": "error",
"no-throw-literal": "error",
"no-trailing-spaces": "error",
"no-undef": "error",
"no-undef-init": "error",
"no-unexpected-multiline": "error",
"no-useless-constructor": "error",
"no-unmodified-loop-condition": "error",
"no-unneeded-ternary": "error",
"no-unreachable": "error",
"no-unused-expressions": "error",
"no-unused-labels": "error",
"no-use-before-define": "error",
"no-useless-call": "error",
"no-dupe-class-members": "error",
"no-var": "error",
"no-void": "error",
"no-with": "error",
"prefer-arrow-callback": "error",
"prefer-const": "error",
"prefer-rest-params": "error",
"prefer-spread": "error",
"prefer-template": "error",
"radix": "error",
"require-yield": "error",
"strict": ["error", "global"],
"use-isnan": "error",
"valid-typeof": "error",
"vars-on-top": "error",
"yoda": ["error", "never"],
"no-unused-vars": "off",
"no-console": ["error", { allow: ["debug", "warn", "error"] }],
// SonarJS is not yet compatible with ESLint 9. Commenting these out
// until it is.
// "sonarjs/cognitive-complexity": ["off", MAX_COGNITIVE_COMPLEXITY],
// "sonarjs/no-duplicate-string": "off",
// "sonarjs/no-nested-template-literals": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
caughtErrorsIgnorePattern: "^_",
},
],
};
export default [
// You would not believe how much this change has frustrated users: ["if an ignores key is used
// without any other keys in the configuration object, then the patterns act as global
// ignores"](https://eslint.org/docs/latest/use/configure/ignore)
{
ignores: [
"dist/",
".wireit/",
"packages/",
// don't ever lint node_modules
"node_modules/",
".storybook/*",
// don't lint build output (make sure it's set to your correct build folder name)
// don't lint nyc coverage output
"coverage/",
"src/locale-codes.ts",
"storybook-static/",
"src/locales/",
"src/**/*.test.ts",
],
},
eslint.configs.recommended,
wcconf.configs["flat/recommended"],
litconf.configs["flat/recommended"],
...tseslint.configs.recommended,
// sonar.configs.recommended,
{
languageOptions: {
parser: tsparser,
parserOptions: {
ecmaVersion: 12,
sourceType: "module",
},
globals: {
...globals.browser,
},
},
files: ["src/**"],
rules,
},
{
languageOptions: {
parser: tsparser,
parserOptions: {
ecmaVersion: 12,
sourceType: "module",
},
globals: {
...globals.nodeBuiltin,
},
},
files: ["scripts/*.mjs", "*.ts", "*.mjs"],
rules,
},
{
languageOptions: {
parser: tsparser,
parserOptions: {
ecmaVersion: 12,
sourceType: "module",
},
globals: {
...globals.nodeBuiltin,
...globals.jest,
},
},
files: ["src/**/*.test.ts"],
rules,
},
];

View File

@ -1,88 +0,0 @@
import eslint from "@eslint/js";
import tsparser from "@typescript-eslint/parser";
import litconf from "eslint-plugin-lit";
import wcconf from "eslint-plugin-wc";
import globals from "globals";
import tseslint from "typescript-eslint";
export default [
// You would not believe how much this change has frustrated users: ["if an ignores key is used
// without any other keys in the configuration object, then the patterns act as global
// ignores"](https://eslint.org/docs/latest/use/configure/ignore)
{
ignores: [
"dist/",
".wireit/",
"packages/",
// don't ever lint node_modules
"node_modules/",
".storybook/*",
// don't lint build output (make sure it's set to your correct build folder name)
// don't lint nyc coverage output
"coverage/",
"src/locale-codes.ts",
"storybook-static/",
"scripts/esbuild",
"src/locales/",
],
},
eslint.configs.recommended,
wcconf.configs["flat/recommended"],
litconf.configs["flat/recommended"],
...tseslint.configs.recommended,
// sonar.configs.recommended,
{
languageOptions: {
parser: tsparser,
parserOptions: {
ecmaVersion: 12,
sourceType: "module",
},
},
files: ["src/**"],
rules: {
"no-unused-vars": "off",
"no-console": ["error", { allow: ["debug", "warn", "error"] }],
// SonarJS is not yet compatible with ESLint 9. Commenting these out
// until it is.
// "sonarjs/cognitive-complexity": ["off", 9],
// "sonarjs/no-duplicate-string": "off",
// "sonarjs/no-nested-template-literals": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
caughtErrorsIgnorePattern: "^_",
},
],
},
},
{
languageOptions: {
parser: tsparser,
parserOptions: {
ecmaVersion: 12,
sourceType: "module",
},
globals: {
...globals.nodeBuiltin,
},
},
files: ["scripts/*.mjs", "*.ts", "*.mjs"],
rules: {
"no-unused-vars": "off",
"no-console": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
caughtErrorsIgnorePattern: "^_",
},
],
},
},
];

View File

@ -1,33 +0,0 @@
#!/usr/bin/env bash
TARGET="./node_modules/@spotlightjs/overlay/dist/index-"[0-9a-f]*.js
if [[ $(grep -L "QX2" "$TARGET" > /dev/null 2> /dev/null) ]]; then
patch --forward -V none --no-backup-if-mismatch -p0 $TARGET <<EOF
TARGET=$(find "./node_modules/@spotlightjs/overlay/dist/" -name "index-[0-9a-f]*.js");
if ! grep -GL 'QX2 = ' "$TARGET" > /dev/null ; then
patch --forward --no-backup-if-mismatch -p0 "$TARGET" <<EOF
>>>>>>> main
--- a/index-5682ce90.js 2024-06-13 16:19:28
+++ b/index-5682ce90.js 2024-06-13 16:20:23
@@ -4958,11 +4958,10 @@
}
);
}
-const q2 = w.lazy(() => import("./main-3257b7fc.js").then((n) => n.m));
+const q2 = w.lazy(() => import("./main-3257b7fc.js").then((n) => n.m)), QX2 = () => {};
function Gp({
data: n,
- onUpdateData: a = () => {
- },
+ onUpdateData: a = QX2,
editingEnabled: s = !1,
clipboardEnabled: o = !1,
displayDataTypes: c = !1,
EOF
else
echo "spotlight overlay.js patch already applied"
fi

View File

@ -1,22 +1,36 @@
import { readFileSync } from "fs";
import path from "path";
/**
* @file Pseudo-localization script.
*
* @import { ConfigFile } from "@lit/localize-tools/lib/types/config.js"
* @import { Config } from '@lit/localize-tools/lib/types/config.js';
* @import { ProgramMessage } from "@lit/localize-tools/src/messages.js"
* @import { Locale } from "@lit/localize-tools/src/types/locale.js"
*/
import { PackageRoot } from "#paths/node";
import { readFileSync } from "node:fs";
import path from "node:path";
import pseudolocale from "pseudolocale";
import { fileURLToPath } from "url";
import { makeFormatter } from "@lit/localize-tools/lib/formatters/index.js";
import { sortProgramMessages } from "@lit/localize-tools/lib/messages.js";
import { TransformLitLocalizer } from "@lit/localize-tools/lib/modes/transform.js";
const __dirname = fileURLToPath(new URL(".", import.meta.url));
const pseudoLocale = "pseudo-LOCALE";
const pseudoLocale = /** @type {Locale} */ ("pseudo-LOCALE");
const targetLocales = [pseudoLocale];
const baseConfig = JSON.parse(readFileSync(path.join(__dirname, "../lit-localize.json"), "utf-8"));
/**
* @type {ConfigFile}
*/
const baseConfig = JSON.parse(readFileSync(path.join(PackageRoot, "lit-localize.json"), "utf-8"));
// Need to make some internal specifications to satisfy the transformer. It doesn't actually matter
// which Localizer we use (transformer or runtime), because all of the functionality we care about
// is in their common parent class, but I had to pick one. Everything else here is just pure
// exploitation of the lit/localize-tools internals.
/**
* @satisfies {Config}
*/
const config = {
...baseConfig,
baseDir: path.join(__dirname, ".."),
@ -28,6 +42,11 @@ const config = {
resolve: (path) => path,
};
/**
*
* @param {ProgramMessage} message
* @returns
*/
const pseudoMessagify = (message) => ({
name: message.name,
contents: message.contents.map((content) =>
@ -36,7 +55,7 @@ const pseudoMessagify = (message) => ({
});
const localizer = new TransformLitLocalizer(config);
const messages = localizer.extractSourceMessages().messages;
const { messages } = localizer.extractSourceMessages();
const translations = messages.map(pseudoMessagify);
const sorted = sortProgramMessages([...messages]);
const formatter = makeFormatter(config);

View File

@ -1,11 +1,9 @@
import { WithBrandConfig } from "#elements/mixins/branding";
import { WithLicenseSummary } from "#elements/mixins/license";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { VERSION } from "@goauthentik/common/constants";
import { globalAK } from "@goauthentik/common/global";
import "@goauthentik/elements/EmptyState";
import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider";
import { WithLicenseSummary } from "@goauthentik/elements/Interface/licenseSummaryProvider";
import { ModalButton } from "@goauthentik/elements/buttons/ModalButton";
import { DefaultBrand } from "@goauthentik/elements/sidebar/SidebarBrand";
import { msg } from "@lit/localize";
import { TemplateResult, css, html } from "lit";
@ -45,7 +43,7 @@ export class AboutModal extends WithLicenseSummary(WithBrandConfig(ModalButton))
}
return [
[msg("Version"), version.versionCurrent],
[msg("UI Version"), VERSION],
[msg("UI Version"), import.meta.env.AK_VERSION],
[msg("Build"), build],
[msg("Python version"), status.runtime.pythonVersion],
[msg("Platform"), status.runtime.platform],
@ -58,8 +56,9 @@ export class AboutModal extends WithLicenseSummary(WithBrandConfig(ModalButton))
}
renderModal() {
let product = globalAK().brand.brandingTitle || DefaultBrand.brandingTitle;
if (this.licenseSummary.status != LicenseSummaryStatusEnum.Unlicensed) {
let product = this.brandingTitle;
if (this.licenseSummary.status !== LicenseSummaryStatusEnum.Unlicensed) {
product += ` ${msg("Enterprise")}`;
}
return html`<div
@ -74,7 +73,7 @@ export class AboutModal extends WithLicenseSummary(WithBrandConfig(ModalButton))
<div class="pf-c-about-modal-box__brand">
<img
class="pf-c-about-modal-box__brand-image"
src=${this.brand?.brandingFavicon ?? DefaultBrand.brandingFavicon}
src=${this.brandingFavicon}
alt="${msg("authentik Logo")}"
/>
</div>

View File

@ -1,187 +0,0 @@
import "@goauthentik/admin/AdminInterface/AboutModal";
import type { AboutModal } from "@goauthentik/admin/AdminInterface/AboutModal";
import { ROUTES } from "@goauthentik/admin/Routes";
import {
EVENT_API_DRAWER_TOGGLE,
EVENT_NOTIFICATION_DRAWER_TOGGLE,
} from "@goauthentik/common/constants";
import { configureSentry } from "@goauthentik/common/sentry";
import { me } from "@goauthentik/common/users";
import { WebsocketClient } from "@goauthentik/common/ws";
import { AuthenticatedInterface } from "@goauthentik/elements/Interface";
import "@goauthentik/elements/ak-locale-context";
import "@goauthentik/elements/banner/EnterpriseStatusBanner";
import "@goauthentik/elements/banner/VersionBanner";
import "@goauthentik/elements/messages/MessageContainer";
import "@goauthentik/elements/messages/MessageContainer";
import "@goauthentik/elements/notifications/APIDrawer";
import "@goauthentik/elements/notifications/NotificationDrawer";
import { getURLParam, updateURLParams } from "@goauthentik/elements/router/RouteMatch";
import "@goauthentik/elements/router/RouterOutlet";
import "@goauthentik/elements/sidebar/Sidebar";
import "@goauthentik/elements/sidebar/SidebarItem";
import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement, property, query, state } from "lit/decorators.js";
import { classMap } from "lit/directives/class-map.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFDrawer from "@patternfly/patternfly/components/Drawer/drawer.css";
import PFPage from "@patternfly/patternfly/components/Page/page.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { SessionUser, UiThemeEnum } from "@goauthentik/api";
import "./AdminSidebar";
if (process.env.NODE_ENV === "development") {
await import("@goauthentik/esbuild-plugin-live-reload/client");
}
@customElement("ak-interface-admin")
export class AdminInterface extends AuthenticatedInterface {
@property({ type: Boolean })
notificationDrawerOpen = getURLParam("notificationDrawerOpen", false);
@property({ type: Boolean })
apiDrawerOpen = getURLParam("apiDrawerOpen", false);
ws: WebsocketClient;
@state()
user?: SessionUser;
@query("ak-about-modal")
aboutModal?: AboutModal;
static get styles(): CSSResult[] {
return [
PFBase,
PFPage,
PFButton,
PFDrawer,
css`
.pf-c-page__main,
.pf-c-drawer__content,
.pf-c-page__drawer {
z-index: auto !important;
background-color: transparent;
}
.display-none {
display: none;
}
.pf-c-page {
background-color: var(--pf-c-page--BackgroundColor) !important;
}
/* Global page background colour */
:host([theme="dark"]) .pf-c-page {
--pf-c-page--BackgroundColor: var(--ak-dark-background);
}
ak-enterprise-status,
ak-version-banner {
grid-area: header;
}
ak-admin-sidebar {
grid-area: nav;
}
.pf-c-drawer__panel {
z-index: var(--pf-global--ZIndex--xl);
}
`,
];
}
constructor() {
super();
this.ws = new WebsocketClient();
window.addEventListener(EVENT_NOTIFICATION_DRAWER_TOGGLE, () => {
this.notificationDrawerOpen = !this.notificationDrawerOpen;
updateURLParams({
notificationDrawerOpen: this.notificationDrawerOpen,
});
});
window.addEventListener(EVENT_API_DRAWER_TOGGLE, () => {
this.apiDrawerOpen = !this.apiDrawerOpen;
updateURLParams({
apiDrawerOpen: this.apiDrawerOpen,
});
});
}
async firstUpdated(): Promise<void> {
configureSentry(true);
this.user = await me();
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/");
}
}
render(): TemplateResult {
const sidebarClasses = {
"pf-m-light": this.activeTheme === UiThemeEnum.Light,
};
const drawerOpen = this.notificationDrawerOpen || this.apiDrawerOpen;
const drawerClasses = {
"pf-m-expanded": drawerOpen,
"pf-m-collapsed": !drawerOpen,
};
return html` <ak-locale-context>
<div class="pf-c-page">
<ak-enterprise-status interface="admin"></ak-enterprise-status>
<ak-version-banner></ak-version-banner>
<ak-admin-sidebar
class="pf-c-page__sidebar ${classMap(sidebarClasses)}"
></ak-admin-sidebar>
<div class="pf-c-page__drawer">
<div class="pf-c-drawer ${classMap(drawerClasses)}">
<div class="pf-c-drawer__main">
<div class="pf-c-drawer__content">
<div class="pf-c-drawer__body">
<main class="pf-c-page__main">
<ak-router-outlet
role="main"
class="pf-c-page__main"
tabindex="-1"
id="main-content"
defaultUrl="/administration/overview"
.routes=${ROUTES}
>
</ak-router-outlet>
</main>
</div>
</div>
<ak-notification-drawer
class="pf-c-drawer__panel pf-m-width-33 ${this
.notificationDrawerOpen
? ""
: "display-none"}"
?hidden=${!this.notificationDrawerOpen}
></ak-notification-drawer>
<ak-api-drawer
class="pf-c-drawer__panel pf-m-width-33 ${this.apiDrawerOpen
? ""
: "display-none"}"
?hidden=${!this.apiDrawerOpen}
></ak-api-drawer>
<ak-about-modal></ak-about-modal>
</div>
</div>
</div></div
></ak-locale-context>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-interface-admin": AdminInterface;
}
}

View File

@ -1,186 +1,97 @@
import { EVENT_SIDEBAR_TOGGLE } from "@goauthentik/common/constants";
import { me } from "@goauthentik/common/users";
import { AKElement } from "@goauthentik/elements/Base";
import {
CapabilitiesEnum,
WithCapabilitiesConfig,
} from "@goauthentik/elements/Interface/capabilitiesProvider";
import { WithVersion } from "@goauthentik/elements/Interface/versionProvider";
import { ID_REGEX, SLUG_REGEX, UUID_REGEX } from "@goauthentik/elements/router/Route";
import { getRootStyle } from "@goauthentik/elements/utils/getRootStyle";
import { spread } from "@open-wc/lit-helpers";
import { msg } from "@lit/localize";
import { TemplateResult, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { map } from "lit/directives/map.js";
import { repeat } from "lit/directives/repeat.js";
import { UiThemeEnum } from "@goauthentik/api";
import type { SessionUser, UserSelf } from "@goauthentik/api";
// 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
children?: SidebarEntry[],
];
@customElement("ak-admin-sidebar")
export class AkAdminSidebar extends WithCapabilitiesConfig(WithVersion(AKElement)) {
@property({ type: Boolean, reflect: true })
open = true;
/**
* Recursively renders a sidebar entry.
*/
export function renderSidebarItem([
path,
label,
attributes,
children,
]: SidebarEntry): TemplateResult {
const properties = Array.isArray(attributes)
? { ".activeWhen": attributes }
: (attributes ?? {});
@state()
impersonation: UserSelf["username"] | null = null;
constructor() {
super();
me().then((user: SessionUser) => {
this.impersonation = user.original ? user.user.username : null;
});
this.toggleOpen = this.toggleOpen.bind(this);
this.checkWidth = this.checkWidth.bind(this);
if (path) {
properties.path = path;
}
// This has to be a bound method so the event listener can be removed on disconnection as
// needed.
toggleOpen() {
this.open = !this.open;
}
checkWidth() {
// This works just fine, but it assumes that the `--ak-sidebar--minimum-auto-width` is in
// REMs. If that changes, this code will have to be adjusted as well.
const minWidth =
parseFloat(getRootStyle("--ak-sidebar--minimum-auto-width")) *
parseFloat(getRootStyle("font-size"));
this.open = window.innerWidth >= minWidth;
}
connectedCallback() {
super.connectedCallback();
window.addEventListener(EVENT_SIDEBAR_TOGGLE, this.toggleOpen);
window.addEventListener("resize", this.checkWidth);
// After connecting to the DOM, we can now perform this check to see if the sidebar should
// be open by default.
this.checkWidth();
}
// The symmetry (☟, ☝) here is critical in that you want to start adding these handlers after
// connection, and removing them before disconnection.
disconnectedCallback() {
window.removeEventListener(EVENT_SIDEBAR_TOGGLE, this.toggleOpen);
window.removeEventListener("resize", this.checkWidth);
super.disconnectedCallback();
}
render() {
return html`
<ak-sidebar
class="pf-c-page__sidebar ${this.open ? "pf-m-expanded" : "pf-m-collapsed"} ${this
.activeTheme === UiThemeEnum.Light
? "pf-m-light"
: ""}"
>
${this.renderSidebarItems()}
</ak-sidebar>
`;
}
updated() {
// This is permissible as`:host.classList` is not one of the properties Lit uses as a
// scheduling trigger. This sort of shenanigans can trigger an loop, in that it will trigger
// a browser reflow, which may trigger some other styling the application is monitoring,
// triggering a re-render which triggers a browser reflow, ad infinitum. But we've been
// living with that since jQuery, and it's both well-known and fortunately rare.
// eslint-disable-next-line wc/no-self-class
this.classList.remove("pf-m-expanded", "pf-m-collapsed");
// eslint-disable-next-line wc/no-self-class
this.classList.add(this.open ? "pf-m-expanded" : "pf-m-collapsed");
}
renderSidebarItems(): TemplateResult {
// 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
children?: SidebarEntry[],
];
// prettier-ignore
const sidebarContent: SidebarEntry[] = [
[null, msg("Dashboards"), { "?expanded": true }, [
["/administration/overview", msg("Overview")],
["/administration/dashboard/users", msg("User Statistics")],
["/administration/system-tasks", msg("System Tasks")]]],
[null, msg("Applications"), null, [
["/core/applications", msg("Applications"), [`^/core/applications/(?<slug>${SLUG_REGEX})$`]],
["/core/providers", msg("Providers"), [`^/core/providers/(?<id>${ID_REGEX})$`]],
["/outpost/outposts", msg("Outposts")]]],
[null, msg("Events"), null, [
["/events/log", msg("Logs"), [`^/events/log/(?<id>${UUID_REGEX})$`]],
["/events/rules", msg("Notification Rules")],
["/events/transports", msg("Notification Transports")]]],
[null, msg("Customization"), null, [
["/policy/policies", msg("Policies")],
["/core/property-mappings", msg("Property Mappings")],
["/blueprints/instances", msg("Blueprints")],
["/policy/reputation", msg("Reputation scores")]]],
[null, msg("Flows and Stages"), null, [
["/flow/flows", msg("Flows"), [`^/flow/flows/(?<slug>${SLUG_REGEX})$`]],
["/flow/stages", msg("Stages")],
["/flow/stages/prompts", msg("Prompts")]]],
[null, msg("Directory"), null, [
["/identity/users", msg("Users"), [`^/identity/users/(?<id>${ID_REGEX})$`]],
["/identity/groups", msg("Groups"), [`^/identity/groups/(?<id>${UUID_REGEX})$`]],
["/identity/roles", msg("Roles"), [`^/identity/roles/(?<id>${UUID_REGEX})$`]],
["/identity/initial-permissions", msg("Initial Permissions"), [`^/identity/initial-permissions/(?<id>${ID_REGEX})$`]],
["/core/sources", msg("Federation and Social login"), [`^/core/sources/(?<slug>${SLUG_REGEX})$`]],
["/core/tokens", msg("Tokens and App passwords")],
["/flow/stages/invitations", msg("Invitations")]]],
[null, msg("System"), null, [
["/core/brands", msg("Brands")],
["/crypto/certificates", msg("Certificates")],
["/outpost/integrations", msg("Outpost Integrations")],
["/admin/settings", msg("Settings")]]],
];
// Typescript requires the type here to correctly type the recursive path
type SidebarRenderer = (_: SidebarEntry) => TemplateResult;
const renderOneSidebarItem: SidebarRenderer = ([path, label, attributes, children]) => {
const properties = Array.isArray(attributes)
? { ".activeWhen": attributes }
: (attributes ?? {});
if (path) {
properties.path = path;
}
return html`<ak-sidebar-item ${spread(properties)}>
${label ? html`<span slot="label">${label}</span>` : nothing}
${map(children, renderOneSidebarItem)}
</ak-sidebar-item>`;
};
// prettier-ignore
return html`
${map(sidebarContent, renderOneSidebarItem)}
${this.renderEnterpriseMenu()}
`;
}
renderEnterpriseMenu() {
return this.can(CapabilitiesEnum.IsEnterprise)
? html`
<ak-sidebar-item>
<span slot="label">${msg("Enterprise")}</span>
<ak-sidebar-item path="/enterprise/licenses">
<span slot="label">${msg("Licenses")}</span>
</ak-sidebar-item>
</ak-sidebar-item>
`
: nothing;
}
return html`<ak-sidebar-item ${spread(properties)}>
${label ? html`<span slot="label">${label}</span>` : nothing}
${children ? renderSidebarItems(children) : nothing}
</ak-sidebar-item>`;
}
declare global {
interface HTMLElementTagNameMap {
"ak-admin-sidebar": AkAdminSidebar;
}
/**
* Recursively renders a collection of sidebar entries.
*/
export function renderSidebarItems(entries: readonly SidebarEntry[]) {
return repeat(entries, ([path, label]) => path || label, renderSidebarItem);
}
// prettier-ignore
export const AdminSidebarEntries: readonly SidebarEntry[] = [
[null, msg("Dashboards"), { "?expanded": true }, [
["/administration/overview", msg("Overview")],
["/administration/dashboard/users", msg("User Statistics")],
["/administration/system-tasks", msg("System Tasks")]]
],
[null, msg("Applications"), null, [
["/core/applications", msg("Applications"), [`^/core/applications/(?<slug>${SLUG_REGEX})$`]],
["/core/providers", msg("Providers"), [`^/core/providers/(?<id>${ID_REGEX})$`]],
["/outpost/outposts", msg("Outposts")]]
],
[null, msg("Events"), null, [
["/events/log", msg("Logs"), [`^/events/log/(?<id>${UUID_REGEX})$`]],
["/events/rules", msg("Notification Rules")],
["/events/transports", msg("Notification Transports")]]
],
[null, msg("Customization"), null, [
["/policy/policies", msg("Policies")],
["/core/property-mappings", msg("Property Mappings")],
["/blueprints/instances", msg("Blueprints")],
["/policy/reputation", msg("Reputation scores")]]
],
[null, msg("Flows and Stages"), null, [
["/flow/flows", msg("Flows"), [`^/flow/flows/(?<slug>${SLUG_REGEX})$`]],
["/flow/stages", msg("Stages")],
["/flow/stages/prompts", msg("Prompts")]]
],
[null, msg("Directory"), null, [
["/identity/users", msg("Users"), [`^/identity/users/(?<id>${ID_REGEX})$`]],
["/identity/groups", msg("Groups"), [`^/identity/groups/(?<id>${UUID_REGEX})$`]],
["/identity/roles", msg("Roles"), [`^/identity/roles/(?<id>${UUID_REGEX})$`]],
["/identity/initial-permissions", msg("Initial Permissions"), [`^/identity/initial-permissions/(?<id>${ID_REGEX})$`]],
["/core/sources", msg("Federation and Social login"), [`^/core/sources/(?<slug>${SLUG_REGEX})$`]],
["/core/tokens", msg("Tokens and App passwords")],
["/flow/stages/invitations", msg("Invitations")]]
],
[null, msg("System"), null, [
["/core/brands", msg("Brands")],
["/crypto/certificates", msg("Certificates")],
["/outpost/integrations", msg("Outpost Integrations")],
["/admin/settings", msg("Settings")]]
],
];
// prettier-ignore
export const AdminSidebarEnterpriseEntries: readonly SidebarEntry[] = [
[null, msg("Enterprise"), null, [
["/enterprise/licenses", msg("Licenses"), null]
],
]]

View File

@ -0,0 +1,253 @@
import "#admin/AdminInterface/AboutModal";
import type { AboutModal } from "#admin/AdminInterface/AboutModal";
import { ROUTES } from "#admin/Routes";
import { EVENT_API_DRAWER_TOGGLE, EVENT_NOTIFICATION_DRAWER_TOGGLE } from "#common/constants";
import { configureSentry } from "#common/sentry/index";
import { me } from "#common/users";
import { WebsocketClient } from "#common/ws";
import { SidebarToggleEventDetail } from "#components/ak-page-header";
import { AuthenticatedInterface } from "#elements/AuthenticatedInterface";
import "#elements/ak-locale-context/ak-locale-context";
import "#elements/banner/EnterpriseStatusBanner";
import "#elements/banner/EnterpriseStatusBanner";
import "#elements/banner/VersionBanner";
import "#elements/banner/VersionBanner";
import "#elements/messages/MessageContainer";
import "#elements/messages/MessageContainer";
import { WithCapabilitiesConfig } from "#elements/mixins/capabilities";
import "#elements/notifications/APIDrawer";
import "#elements/notifications/NotificationDrawer";
import { getURLParam, updateURLParams } from "#elements/router/RouteMatch";
import "#elements/router/RouterOutlet";
import "#elements/sidebar/Sidebar";
import "#elements/sidebar/SidebarItem";
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";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFDrawer from "@patternfly/patternfly/components/Drawer/drawer.css";
import PFNav from "@patternfly/patternfly/components/Nav/nav.css";
import PFPage from "@patternfly/patternfly/components/Page/page.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { CapabilitiesEnum, SessionUser, UiThemeEnum } from "@goauthentik/api";
import {
AdminSidebarEnterpriseEntries,
AdminSidebarEntries,
renderSidebarItems,
} from "./AdminSidebar.js";
if (process.env.NODE_ENV === "development") {
await import("@goauthentik/esbuild-plugin-live-reload/client");
}
@customElement("ak-interface-admin")
export class AdminInterface extends WithCapabilitiesConfig(AuthenticatedInterface) {
//#region Properties
@property({ type: Boolean })
public notificationDrawerOpen = getURLParam("notificationDrawerOpen", false);
@property({ type: Boolean })
public apiDrawerOpen = getURLParam("apiDrawerOpen", false);
protected readonly ws: WebsocketClient;
@property({
type: Object,
attribute: false,
reflect: false,
})
public user?: SessionUser;
@query("ak-about-modal")
public aboutModal?: AboutModal;
@property({ type: Boolean, reflect: true })
public sidebarOpen: boolean;
@eventOptions({ passive: true })
protected sidebarListener(event: CustomEvent<SidebarToggleEventDetail>) {
this.sidebarOpen = !!event.detail.open;
}
#sidebarMatcher: MediaQueryList;
#sidebarMediaQueryListener = (event: MediaQueryListEvent) => {
this.sidebarOpen = event.matches;
};
//#endregion
//#region Styles
static styles: CSSResult[] = [
PFBase,
PFPage,
PFButton,
PFDrawer,
PFNav,
css`
.pf-c-page__main,
.pf-c-drawer__content,
.pf-c-page__drawer {
z-index: auto !important;
background-color: transparent;
}
.display-none {
display: none;
}
.pf-c-page {
background-color: var(--pf-c-page--BackgroundColor) !important;
}
:host([theme="dark"]) {
/* Global page background colour */
.pf-c-page {
--pf-c-page--BackgroundColor: var(--ak-dark-background);
}
}
ak-page-navbar {
grid-area: header;
}
.ak-sidebar {
grid-area: nav;
}
.pf-c-drawer__panel {
z-index: var(--pf-global--ZIndex--xl);
}
`,
];
//#endregion
//#region Lifecycle
constructor() {
configureSentry(true);
super();
this.ws = new WebsocketClient();
this.#sidebarMatcher = window.matchMedia("(min-width: 1200px)");
this.sidebarOpen = this.#sidebarMatcher.matches;
}
public connectedCallback() {
super.connectedCallback();
window.addEventListener(EVENT_NOTIFICATION_DRAWER_TOGGLE, () => {
this.notificationDrawerOpen = !this.notificationDrawerOpen;
updateURLParams({
notificationDrawerOpen: this.notificationDrawerOpen,
});
});
window.addEventListener(EVENT_API_DRAWER_TOGGLE, () => {
this.apiDrawerOpen = !this.apiDrawerOpen;
updateURLParams({
apiDrawerOpen: this.apiDrawerOpen,
});
});
this.#sidebarMatcher.addEventListener("change", this.#sidebarMediaQueryListener, {
passive: true,
});
}
public disconnectedCallback(): void {
super.disconnectedCallback();
this.#sidebarMatcher.removeEventListener("change", this.#sidebarMediaQueryListener);
}
async firstUpdated(): Promise<void> {
this.user = await me();
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/");
}
}
render(): TemplateResult {
const sidebarClasses = {
"pf-c-page__sidebar": true,
"pf-m-light": this.activeTheme === UiThemeEnum.Light,
"pf-m-expanded": this.sidebarOpen,
"pf-m-collapsed": !this.sidebarOpen,
};
const drawerOpen = this.notificationDrawerOpen || this.apiDrawerOpen;
const drawerClasses = {
"pf-m-expanded": drawerOpen,
"pf-m-collapsed": !drawerOpen,
};
return html` <ak-locale-context>
<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)}">
${renderSidebarItems(AdminSidebarEntries)}
${this.can(CapabilitiesEnum.IsEnterprise)
? renderSidebarItems(AdminSidebarEnterpriseEntries)
: nothing}
</ak-sidebar>
<div class="pf-c-page__drawer">
<div class="pf-c-drawer ${classMap(drawerClasses)}">
<div class="pf-c-drawer__main">
<div class="pf-c-drawer__content">
<div class="pf-c-drawer__body">
<main class="pf-c-page__main">
<ak-router-outlet
role="main"
class="pf-c-page__main"
tabindex="-1"
id="main-content"
defaultUrl="/administration/overview"
.routes=${ROUTES}
>
</ak-router-outlet>
</main>
</div>
</div>
<ak-notification-drawer
class="pf-c-drawer__panel pf-m-width-33 ${this
.notificationDrawerOpen
? ""
: "display-none"}"
?hidden=${!this.notificationDrawerOpen}
></ak-notification-drawer>
<ak-api-drawer
class="pf-c-drawer__panel pf-m-width-33 ${this.apiDrawerOpen
? ""
: "display-none"}"
?hidden=${!this.apiDrawerOpen}
></ak-api-drawer>
<ak-about-modal></ak-about-modal>
</div>
</div>
</div></div
></ak-locale-context>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-interface-admin": AdminInterface;
}
}

View File

@ -1,5 +0,0 @@
import { AdminInterface } from "./AdminInterface";
import "./AdminInterface";
export { AdminInterface };
export default AdminInterface;

View File

@ -1,9 +1,9 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { parseAPIResponseError, pluckErrorDetail } from "@goauthentik/common/errors/network";
import { MessageLevel } from "@goauthentik/common/messages";
import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/PageHeader";
import { showMessage } from "@goauthentik/elements/messages/MessageContainer";
import { DEFAULT_CONFIG } from "#common/api/config";
import { parseAPIResponseError, pluckErrorDetail } from "#common/errors/network";
import { MessageLevel } from "#common/messages";
import "#components/ak-page-header";
import { AKElement } from "#elements/Base";
import { showMessage } from "#elements/messages/MessageContainer";
import * as Sentry from "@sentry/browser";
import { CSSResult, TemplateResult, html } from "lit";

View File

@ -1,29 +1,27 @@
import "@goauthentik/admin/admin-overview/TopApplicationsTable";
import "@goauthentik/admin/admin-overview/cards/AdminStatusCard";
import "@goauthentik/admin/admin-overview/cards/FipsStatusCard";
import "@goauthentik/admin/admin-overview/cards/RecentEventsCard";
import "@goauthentik/admin/admin-overview/cards/SystemStatusCard";
import "@goauthentik/admin/admin-overview/cards/VersionStatusCard";
import "@goauthentik/admin/admin-overview/cards/WorkerStatusCard";
import "@goauthentik/admin/admin-overview/charts/AdminLoginAuthorizeChart";
import "@goauthentik/admin/admin-overview/charts/OutpostStatusChart";
import "@goauthentik/admin/admin-overview/charts/SyncStatusChart";
import { VERSION } from "@goauthentik/common/constants";
import { me } from "@goauthentik/common/users";
import { AKElement } from "@goauthentik/elements/Base";
import { WithLicenseSummary } from "@goauthentik/elements/Interface/licenseSummaryProvider.js";
import "@goauthentik/elements/PageHeader";
import "@goauthentik/elements/cards/AggregatePromiseCard";
import "@goauthentik/elements/cards/QuickActionsCard.js";
import type { QuickAction } from "@goauthentik/elements/cards/QuickActionsCard.js";
import { paramURL } from "@goauthentik/elements/router/RouterOutlet";
import "#admin/admin-overview/TopApplicationsTable";
import "#admin/admin-overview/cards/AdminStatusCard";
import "#admin/admin-overview/cards/FipsStatusCard";
import "#admin/admin-overview/cards/RecentEventsCard";
import "#admin/admin-overview/cards/SystemStatusCard";
import "#admin/admin-overview/cards/VersionStatusCard";
import "#admin/admin-overview/cards/WorkerStatusCard";
import "#admin/admin-overview/charts/AdminLoginAuthorizeChart";
import "#admin/admin-overview/charts/OutpostStatusChart";
import "#admin/admin-overview/charts/SyncStatusChart";
import { me } from "#common/users";
import "#components/ak-page-header";
import { AKElement } from "#elements/Base";
import "#elements/cards/AggregatePromiseCard";
import type { QuickAction } from "#elements/cards/QuickActionsCard";
import "#elements/cards/QuickActionsCard";
import { WithLicenseSummary } from "#elements/mixins/license";
import { paramURL } from "#elements/router/RouterOutlet";
import { createReleaseNotesURL } from "@goauthentik/core/version";
import { msg, str } from "@lit/localize";
import { CSSResult, TemplateResult, css, html, nothing } from "lit";
import { customElement, state } from "lit/decorators.js";
import { classMap } from "lit/directives/class-map.js";
import { map } from "lit/directives/map.js";
import { when } from "lit/directives/when.js";
import PFContent from "@patternfly/patternfly/components/Content/content.css";
import PFDivider from "@patternfly/patternfly/components/Divider/divider.css";
@ -33,21 +31,8 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { SessionUser } from "@goauthentik/api";
export function versionFamily(): string {
const parts = VERSION.split(".");
parts.pop();
return parts.join(".");
}
const RELEASE = `${VERSION.split(".").slice(0, -1).join(".")}#fixed-in-${VERSION.replaceAll(
".",
"",
)}`;
const AdminOverviewBase = WithLicenseSummary(AKElement);
type Renderer = () => TemplateResult | typeof nothing;
@customElement("ak-admin-overview")
export class AdminOverviewPage extends AdminOverviewBase {
static get styles(): CSSResult[] {
@ -83,7 +68,11 @@ export class AdminOverviewPage extends AdminOverviewBase {
[msg("Check the logs"), paramURL("/events/log")],
[msg("Explore integrations"), "https://goauthentik.io/integrations/", true],
[msg("Manage users"), paramURL("/identity/users")],
[msg("Check the release notes"), `https://goauthentik.io/docs/releases/${RELEASE}`, true],
[
msg("Check the release notes"),
createReleaseNotesURL(import.meta.env.AK_VERSION).href,
true,
],
];
@state()
@ -94,10 +83,13 @@ export class AdminOverviewPage extends AdminOverviewBase {
}
render(): TemplateResult {
const name = this.user?.user.name ?? this.user?.user.username;
const username = this.user?.user.name || this.user?.user.username;
return html`<ak-page-header description=${msg("General system status")} ?hasIcon=${false}>
<span slot="header"> ${msg(str`Welcome, ${name || ""}.`)} </span>
return html` <ak-page-header
header=${msg(str`Welcome, ${username || ""}.`)}
description=${msg("General system status")}
?hasIcon=${false}
>
</ak-page-header>
<section class="pf-c-page__main-section">
<div class="pf-l-grid pf-m-gutter">
@ -190,45 +182,6 @@ export class AdminOverviewPage extends AdminOverviewBase {
</div>`
: nothing} `;
}
renderActions() {
const release = `${versionFamily()}#fixed-in-${VERSION.replaceAll(".", "")}`;
const quickActions: [string, string][] = [
[msg("Create a new application"), paramURL("/core/applications", { createForm: true })],
[msg("Check the logs"), paramURL("/events/log")],
[msg("Explore integrations"), "https://goauthentik.io/integrations/"],
[msg("Manage users"), paramURL("/identity/users")],
[msg("Check the release notes"), `https://goauthentik.io/docs/releases/${release}`],
];
const action = ([label, url]: [string, string]) => {
const isExternal = url.startsWith("https://");
const ex = (truecase: Renderer, falsecase: Renderer) =>
when(isExternal, truecase, falsecase);
const content = html`${label}${ex(
() => html`<i class="fas fa-external-link-alt ak-external-link"></i>`,
() => nothing,
)}`;
return html`<li>
${ex(
() =>
html`<a
href="${url}"
class="pf-u-mb-xl"
rel="noopener noreferrer"
target="_blank"
>${content}</a
>`,
() => html`<a href="${url}" class="pf-u-mb-xl" )>${content}</a>`,
)}
</li>`;
};
return html`${map(quickActions, action)}`;
}
}
declare global {

View File

@ -1,7 +1,7 @@
import "@goauthentik/admin/admin-overview/charts/AdminModelPerDay";
import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/PageHeader";
import "@goauthentik/elements/cards/AggregatePromiseCard";
import "#admin/admin-overview/charts/AdminModelPerDay";
import "#components/ak-page-header";
import { AKElement } from "#elements/Base";
import "#elements/cards/AggregatePromiseCard";
import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, css, html } from "lit";

View File

@ -46,7 +46,7 @@ export class SystemStatusCard extends AdminStatusCard<SystemInfo> {
return;
}
const outpost = outposts.results[0];
outpost.config["authentik_host"] = window.location.origin;
outpost.config.authentik_host = window.location.origin;
await new OutpostsApi(DEFAULT_CONFIG).outpostsInstancesUpdate({
uuid: outpost.pk,
outpostRequest: outpost,

View File

@ -33,11 +33,10 @@ export class WorkersStatusCard extends AdminStatusCard<Worker[]> {
icon: "fa fa-times-circle pf-m-danger",
message: html`${msg("Worker with incorrect version connected.")}`,
});
} else {
return Promise.resolve<AdminStatus>({
icon: "fa fa-check-circle pf-m-success",
});
}
return Promise.resolve<AdminStatus>({
icon: "fa fa-check-circle pf-m-success",
});
}
renderValue() {

View File

@ -1,5 +1,4 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-number-input";
import "@goauthentik/components/ak-switch-input";
import "@goauthentik/components/ak-text-input";
@ -184,20 +183,14 @@ export class AdminSettingsForm extends Form<SettingsRequest> {
label=${msg("Reputation: lower limit")}
required
name="reputationLowerLimit"
value="${first(
this._settings?.reputationLowerLimit,
DEFAULT_REPUTATION_LOWER_LIMIT,
)}"
value="${this._settings?.reputationLowerLimit ?? DEFAULT_REPUTATION_LOWER_LIMIT}"
help=${msg("Reputation cannot decrease lower than this value. Zero or negative.")}
></ak-number-input>
<ak-number-input
label=${msg("Reputation: upper limit")}
required
name="reputationUpperLimit"
value="${first(
this._settings?.reputationUpperLimit,
DEFAULT_REPUTATION_UPPER_LIMIT,
)}"
value="${this._settings?.reputationUpperLimit ?? DEFAULT_REPUTATION_UPPER_LIMIT}"
help=${msg("Reputation cannot increase higher than this value. Zero or positive.")}
></ak-number-input>
<ak-form-element-horizontal label=${msg("Footer links")} name="footerLinks">
@ -257,7 +250,7 @@ export class AdminSettingsForm extends Form<SettingsRequest> {
label=${msg("Default token length")}
required
name="defaultTokenLength"
value="${first(this._settings?.defaultTokenLength, 60)}"
value="${this._settings?.defaultTokenLength ?? 60}"
help=${msg("Default length of generated tokens")}
></ak-number-input>
`;

View File

@ -1,16 +1,15 @@
import "@goauthentik/admin/admin-settings/AdminSettingsForm";
import { AdminSettingsForm } from "@goauthentik/admin/admin-settings/AdminSettingsForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import "@goauthentik/components/events/ObjectChangelog";
import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/EmptyState";
import "@goauthentik/elements/PageHeader";
import "@goauthentik/elements/Tabs";
import "@goauthentik/elements/buttons/ModalButton";
import "@goauthentik/elements/buttons/SpinnerButton";
import "@goauthentik/elements/buttons/SpinnerButton";
import "@goauthentik/elements/forms/ModalForm";
import "#admin/admin-settings/AdminSettingsForm";
import { AdminSettingsForm } from "#admin/admin-settings/AdminSettingsForm";
import { DEFAULT_CONFIG } from "#common/api/config";
import "#components/ak-page-header";
import "#components/events/ObjectChangelog";
import { AKElement } from "#elements/Base";
import "#elements/CodeMirror";
import "#elements/EmptyState";
import "#elements/Tabs";
import "#elements/buttons/ModalButton";
import "#elements/buttons/SpinnerButton/ak-spinner-button";
import "#elements/forms/ModalForm";
import { msg } from "@lit/localize";
import { html, nothing } from "lit";
@ -83,13 +82,10 @@ export class AdminSettingsPage extends AKElement {
}
render() {
if (!this.settings) {
return nothing;
}
if (!this.settings) return nothing;
return html`
<ak-page-header icon="fa fa-cog" header="" description="">
<span slot="header"> ${msg("System settings")} </span>
</ak-page-header>
<ak-page-header icon="fa fa-cog" header="${msg("System settings")}"> </ak-page-header>
<section class="pf-c-page__main-section pf-m-no-padding-mobile pf-l-grid pf-m-gutter">
<div class="pf-c-card">
<div class="pf-c-card__body">

View File

@ -9,9 +9,9 @@ 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$"]) {
if (document.body._$litPart$) {
// @ts-expect-error expression of type '"_$litPart$"' is added by Lit
await delete document.body["_$litPart$"];
await delete document.body._$litPart$;
}
});
});

View File

@ -1,24 +1,20 @@
import { CapabilitiesEnum, WithCapabilitiesConfig } from "#elements/mixins/capabilities";
import "@goauthentik/admin/applications/ProviderSelectModal";
import { iconHelperText } from "@goauthentik/admin/helperText";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-file-input";
import "@goauthentik/components/ak-radio-input";
import "@goauthentik/components/ak-switch-input";
import "@goauthentik/components/ak-text-input";
import "@goauthentik/components/ak-textarea-input";
import "@goauthentik/elements/Alert.js";
import {
CapabilitiesEnum,
WithCapabilitiesConfig,
} from "@goauthentik/elements/Interface/capabilitiesProvider";
import "@goauthentik/elements/Alert";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/ModalForm";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
import "@goauthentik/elements/forms/ProxyForm";
import "@goauthentik/elements/forms/Radio";
import "@goauthentik/elements/forms/SearchSelect";
import "@goauthentik/elements/forms/SearchSelect/ak-search-select";
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
import { msg } from "@lit/localize";
@ -79,7 +75,7 @@ export class ApplicationForm extends WithCapabilitiesConfig(ModelForm<Applicatio
});
}
if (this.can(CapabilitiesEnum.CanSaveMedia)) {
const icon = this.getFormFiles()["metaIcon"];
const icon = this.getFormFiles().metaIcon;
if (icon || this.clearIcon) {
await new CoreApi(DEFAULT_CONFIG).coreApplicationsSetIconCreate({
slug: app.slug,
@ -194,7 +190,7 @@ export class ApplicationForm extends WithCapabilitiesConfig(ModelForm<Applicatio
></ak-text-input>
<ak-switch-input
name="openInNewTab"
?checked=${first(this.instance?.openInNewTab, false)}
?checked=${this.instance?.openInNewTab ?? false}
label=${msg("Open in new tab")}
help=${msg(
"If checked, the launch URL will open in a new browser tab or window from the user's application library.",
@ -221,7 +217,7 @@ export class ApplicationForm extends WithCapabilitiesConfig(ModelForm<Applicatio
: html` <ak-text-input
label=${msg("Icon")}
name="metaIcon"
value=${first(this.instance?.metaIcon, "")}
value=${this.instance?.metaIcon ?? ""}
help=${iconHelperText}
>
</ak-text-input>`}

View File

@ -1,17 +1,17 @@
import "@goauthentik/admin/applications/ApplicationForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import MDApplication from "@goauthentik/docs/add-secure-apps/applications/index.md";
import "@goauthentik/elements/AppIcon.js";
import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider";
import "@goauthentik/elements/ak-mdx";
import "@goauthentik/elements/buttons/SpinnerButton";
import "@goauthentik/elements/forms/DeleteBulkForm";
import "@goauthentik/elements/forms/ModalForm";
import { getURLParam } from "@goauthentik/elements/router/RouteMatch";
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
import { TableColumn } from "@goauthentik/elements/table/Table";
import { TablePage } from "@goauthentik/elements/table/TablePage";
import "#admin/applications/ApplicationForm";
import { DEFAULT_CONFIG } from "#common/api/config";
import "#elements/AppIcon";
import "#elements/ak-mdx/ak-mdx";
import "#elements/buttons/SpinnerButton/ak-spinner-button";
import "#elements/forms/DeleteBulkForm";
import "#elements/forms/ModalForm";
import { WithBrandConfig } from "#elements/mixins/branding";
import { getURLParam } from "#elements/router/RouteMatch";
import { PaginatedResponse } from "#elements/table/Table";
import { TableColumn } from "#elements/table/Table";
import { TablePage } from "#elements/table/TablePage";
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
import MDApplication from "~docs/add-secure-apps/applications/index.md";
import { msg, str } from "@lit/localize";
import { CSSResult, TemplateResult, css, html } from "lit";
@ -22,7 +22,7 @@ import PFCard from "@patternfly/patternfly/components/Card/card.css";
import { Application, CoreApi, PoliciesApi } from "@goauthentik/api";
import "./ApplicationWizardHint";
import "./ApplicationWizardHint.js";
export const applicationListStyle = css`
/* Fix alignment issues with images in tables */
@ -50,7 +50,7 @@ export class ApplicationListPage extends WithBrandConfig(TablePage<Application>)
}
pageDescription(): string {
return msg(
str`External applications that use ${this.brand?.brandingTitle ?? "authentik"} as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.`,
str`External applications that use ${this.brandingTitle} as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.`,
);
}
pageIcon(): string {

View File

@ -1,18 +1,18 @@
import "@goauthentik/admin/applications/ApplicationAuthorizeChart";
import "@goauthentik/admin/applications/ApplicationCheckAccessForm";
import "@goauthentik/admin/applications/ApplicationForm";
import "@goauthentik/admin/applications/entitlements/ApplicationEntitlementPage";
import "@goauthentik/admin/policies/BoundPoliciesList";
import "@goauthentik/admin/rbac/ObjectPermissionsPage";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { PFSize } from "@goauthentik/common/enums.js";
import "@goauthentik/components/events/ObjectChangelog";
import "@goauthentik/elements/AppIcon";
import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/EmptyState";
import "@goauthentik/elements/PageHeader";
import "@goauthentik/elements/Tabs";
import "@goauthentik/elements/buttons/SpinnerButton";
import "#admin/applications/ApplicationAuthorizeChart";
import "#admin/applications/ApplicationCheckAccessForm";
import "#admin/applications/ApplicationForm";
import "#admin/applications/entitlements/ApplicationEntitlementPage";
import "#admin/policies/BoundPoliciesList";
import "#admin/rbac/ObjectPermissionsPage";
import { DEFAULT_CONFIG } from "#common/api/config";
import { PFSize } from "#common/enums";
import "#components/ak-page-header";
import "#components/events/ObjectChangelog";
import "#elements/AppIcon";
import { AKElement } from "#elements/Base";
import "#elements/EmptyState";
import "#elements/Tabs";
import "#elements/buttons/SpinnerButton/ak-spinner-button";
import { msg } from "@lit/localize";
import { CSSResult, PropertyValues, TemplateResult, html } from "lit";
@ -113,8 +113,7 @@ export class ApplicationViewPage extends AKElement {
renderApp(): TemplateResult {
if (!this.application) {
return html`<ak-empty-state ?loading="${true}" header=${msg("Loading")}>
</ak-empty-state>`;
return html`<ak-empty-state loading header=${msg("Loading")}> </ak-empty-state>`;
}
return html`<ak-tabs>
${this.missingOutpost

View File

@ -1,5 +1,4 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/CodeMirror";
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/forms/HorizontalFormElement";
@ -31,9 +30,8 @@ export class ApplicationEntitlementForm extends ModelForm<ApplicationEntitlement
getSuccessMessage(): string {
if (this.instance?.pbmUuid) {
return msg("Successfully updated entitlement.");
} else {
return msg("Successfully created entitlement.");
}
return msg("Successfully created entitlement.");
}
static get styles(): CSSResult[] {
@ -49,18 +47,17 @@ export class ApplicationEntitlementForm extends ModelForm<ApplicationEntitlement
pbmUuid: this.instance.pbmUuid || "",
applicationEntitlementRequest: data,
});
} else {
return new CoreApi(DEFAULT_CONFIG).coreApplicationEntitlementsCreate({
applicationEntitlementRequest: data,
});
}
return new CoreApi(DEFAULT_CONFIG).coreApplicationEntitlementsCreate({
applicationEntitlementRequest: data,
});
}
renderForm(): TemplateResult {
return html` <ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">
<input
type="text"
value="${first(this.instance?.name, "")}"
value="${this.instance?.name ?? ""}"
class="pf-c-form-control"
required
/>
@ -72,7 +69,7 @@ export class ApplicationEntitlementForm extends ModelForm<ApplicationEntitlement
>
<ak-codemirror
mode=${CodeMirrorMode.YAML}
value="${YAML.stringify(first(this.instance?.attributes, {}))}"
value="${YAML.stringify(this.instance?.attributes ?? {})}"
>
</ak-codemirror>
<p class="pf-c-form__helper-text">

View File

@ -3,5 +3,5 @@ import { createContext } from "@lit/context";
import { LocalTypeCreate } from "./steps/ProviderChoices.js";
export const applicationWizardProvidersContext = createContext<LocalTypeCreate[]>(
Symbol("ak-application-wizard-providers-context"),
Symbol.for("ak-application-wizard-providers-context"),
);

View File

@ -1,7 +1,6 @@
import { policyOptions } from "@goauthentik/admin/applications/PolicyOptions.js";
import { ApplicationWizardStep } from "@goauthentik/admin/applications/wizard/ApplicationWizardStep.js";
import "@goauthentik/admin/applications/wizard/ak-wizard-title.js";
import { isSlug } from "@goauthentik/common/utils.js";
import { camelToSnake } from "@goauthentik/common/utils.js";
import "@goauthentik/components/ak-radio-input";
import "@goauthentik/components/ak-slug-input";
@ -11,6 +10,7 @@ import { type NavigableButton, type WizardButton } from "@goauthentik/components
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 { msg } from "@lit/localize";
import { html } from "lit";
@ -61,18 +61,18 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
this.errors = new Map();
const values = trimMany(this.formValues ?? {}, ["metaLaunchUrl", "name", "slug"]);
if (values["name"] === "") {
if (values.name === "") {
this.errors.set("name", msg("An application name is required"));
}
if (
!(
isStr(values["metaLaunchUrl"]) &&
(values["metaLaunchUrl"] === "" || URL.canParse(values["metaLaunchUrl"]))
isStr(values.metaLaunchUrl) &&
(values.metaLaunchUrl === "" || URL.canParse(values.metaLaunchUrl))
)
) {
this.errors.set("metaLaunchUrl", msg("Not a valid URL"));
}
if (!(isStr(values["slug"]) && values["slug"] !== "" && isSlug(values["slug"]))) {
if (!(isStr(values.slug) && values.slug !== "" && isSlug(values.slug))) {
this.errors.set("slug", msg("Not a valid slug"));
}
return this.errors.size === 0;

View File

@ -1,8 +1,8 @@
import { WithLicenseSummary } from "#elements/mixins/license";
import { ApplicationWizardStep } from "@goauthentik/admin/applications/wizard/ApplicationWizardStep.js";
import "@goauthentik/admin/applications/wizard/ak-wizard-title.js";
import type { NavigableButton, WizardButton } from "@goauthentik/components/ak-wizard/types";
import "@goauthentik/elements/EmptyState.js";
import { WithLicenseSummary } from "@goauthentik/elements/Interface/licenseSummaryProvider.js";
import { bound } from "@goauthentik/elements/decorators/bound.js";
import "@goauthentik/elements/forms/FormGroup.js";
import "@goauthentik/elements/forms/HorizontalFormElement.js";
@ -21,7 +21,7 @@ import { type LocalTypeCreate } from "./ProviderChoices.js";
@customElement("ak-application-wizard-provider-choice-step")
export class ApplicationWizardProviderChoiceStep extends WithLicenseSummary(ApplicationWizardStep) {
label = msg("Choose A Provider");
label = msg("Choose a Provider");
@state()
failureMessage = "";

View File

@ -1,7 +1,7 @@
import { WithBrandConfig } from "#elements/mixins/branding";
import "@goauthentik/admin/applications/wizard/ak-wizard-title.js";
import { ValidationRecord } from "@goauthentik/admin/applications/wizard/types";
import { renderForm } from "@goauthentik/admin/providers/ldap/LDAPProviderFormForm.js";
import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider.js";
import { msg } from "@lit/localize";
import { html } from "lit";

View File

@ -1,7 +1,7 @@
import { WithBrandConfig } from "#elements/mixins/branding";
import "@goauthentik/admin/applications/wizard/ak-wizard-title.js";
import { ValidationRecord } from "@goauthentik/admin/applications/wizard/types";
import { renderForm } from "@goauthentik/admin/providers/radius/RadiusProviderFormForm.js";
import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider";
import { msg } from "@lit/localize";
import { customElement } from "@lit/reactive-element/decorators.js";

View File

@ -1,6 +1,5 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { docLink } from "@goauthentik/common/global";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-toggle-group";
import "@goauthentik/elements/CodeMirror";
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";
@ -59,11 +58,10 @@ export class BlueprintForm extends ModelForm<BlueprintInstance, string> {
instanceUuid: this.instance.pk,
blueprintInstanceRequest: data,
});
} else {
return new ManagedApi(DEFAULT_CONFIG).managedBlueprintsCreate({
blueprintInstanceRequest: data,
});
}
return new ManagedApi(DEFAULT_CONFIG).managedBlueprintsCreate({
blueprintInstanceRequest: data,
});
}
renderForm(): TemplateResult {
@ -80,7 +78,7 @@ export class BlueprintForm extends ModelForm<BlueprintInstance, string> {
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.enabled, true)}
?checked=${this.instance?.enabled ?? true}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
@ -184,7 +182,7 @@ export class BlueprintForm extends ModelForm<BlueprintInstance, string> {
<ak-form-element-horizontal label=${msg("Context")} name="context">
<ak-codemirror
mode=${CodeMirrorMode.YAML}
value="${YAML.stringify(first(this.instance?.context, {}))}"
value="${YAML.stringify(this.instance?.context ?? {})}"
>
</ak-codemirror>
<p class="pf-c-form__helper-text">

View File

@ -1,14 +1,16 @@
import { certificateProvider, certificateSelector } from "@goauthentik/admin/brands/Certificates";
import "@goauthentik/admin/common/ak-crypto-certificate-search";
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import { DefaultBrand } from "@goauthentik/common/ui/config";
import "@goauthentik/elements/CodeMirror";
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-provider.js";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
import "@goauthentik/elements/forms/SearchSelect";
import { DefaultBrand } from "@goauthentik/elements/sidebar/SidebarBrand";
import YAML from "yaml";
import { msg } from "@lit/localize";
@ -43,18 +45,17 @@ export class BrandForm extends ModelForm<Brand, string> {
brandUuid: this.instance.brandUuid,
brandRequest: data,
});
} else {
return new CoreApi(DEFAULT_CONFIG).coreBrandsCreate({
brandRequest: data,
});
}
return new CoreApi(DEFAULT_CONFIG).coreBrandsCreate({
brandRequest: data,
});
}
renderForm(): TemplateResult {
return html` <ak-form-element-horizontal label=${msg("Domain")} required name="domain">
<input
type="text"
value="${first(this.instance?.domain, window.location.host)}"
value="${this.instance?.domain ?? window.location.host}"
class="pf-c-form-control pf-m-monospace"
autocomplete="off"
spellcheck="false"
@ -72,7 +73,7 @@ export class BrandForm extends ModelForm<Brand, string> {
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?._default, false)}
?checked=${this.instance?._default ?? false}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
@ -92,10 +93,7 @@ export class BrandForm extends ModelForm<Brand, string> {
<ak-form-element-horizontal label=${msg("Title")} required name="brandingTitle">
<input
type="text"
value="${first(
this.instance?.brandingTitle,
DefaultBrand.brandingTitle,
)}"
value="${this.instance?.brandingTitle ?? DefaultBrand.brandingTitle}"
class="pf-c-form-control"
required
/>
@ -106,7 +104,7 @@ export class BrandForm extends ModelForm<Brand, string> {
<ak-form-element-horizontal label=${msg("Logo")} required name="brandingLogo">
<input
type="text"
value="${first(this.instance?.brandingLogo, DefaultBrand.brandingLogo)}"
value="${this.instance?.brandingLogo ?? DefaultBrand.brandingLogo}"
class="pf-c-form-control pf-m-monospace"
autocomplete="off"
spellcheck="false"
@ -123,10 +121,8 @@ export class BrandForm extends ModelForm<Brand, string> {
>
<input
type="text"
value="${first(
this.instance?.brandingFavicon,
DefaultBrand.brandingFavicon,
)}"
value="${this.instance?.brandingFavicon ??
DefaultBrand.brandingFavicon}"
class="pf-c-form-control pf-m-monospace"
autocomplete="off"
spellcheck="false"
@ -143,10 +139,8 @@ export class BrandForm extends ModelForm<Brand, string> {
>
<input
type="text"
value="${first(
this.instance?.brandingDefaultFlowBackground,
"/static/dist/assets/images/flow_background.jpg",
)}"
value="${this.instance?.brandingDefaultFlowBackground ??
"/static/dist/assets/images/flow_background.jpg"}"
class="pf-c-form-control pf-m-monospace"
autocomplete="off"
spellcheck="false"
@ -165,10 +159,8 @@ export class BrandForm extends ModelForm<Brand, string> {
>
<ak-codemirror
mode=${CodeMirrorMode.CSS}
value="${first(
this.instance?.brandingCustomCss,
DefaultBrand.brandingCustomCss,
)}"
value="${this.instance?.brandingCustomCss ??
DefaultBrand.brandingCustomCss}"
>
</ak-codemirror>
<p class="pf-c-form__helper-text">
@ -314,10 +306,21 @@ export class BrandForm extends ModelForm<Brand, string> {
.certificate=${this.instance?.webCertificate}
></ak-crypto-certificate-search>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Client Certificates")}
name="clientCertificates"
>
<ak-dual-select-dynamic-selected
.provider=${certificateProvider}
.selector=${certificateSelector(this.instance?.clientCertificates)}
available-label=${msg("Available Certificates")}
selected-label=${msg("Selected Certificates")}
></ak-dual-select-dynamic-selected>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Attributes")} name="attributes">
<ak-codemirror
mode=${CodeMirrorMode.YAML}
value="${YAML.stringify(first(this.instance?.attributes, {}))}"
value="${YAML.stringify(this.instance?.attributes ?? {})}"
>
</ak-codemirror>
<p class="pf-c-form__helper-text">

View File

@ -0,0 +1,39 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { CertificateKeyPair, CryptoApi } from "@goauthentik/api";
const certToSelect = (s: CertificateKeyPair) => [s.pk, s.name, s.name, s];
export async function certificateProvider(page = 1, search = "") {
const certificates = await new CryptoApi(DEFAULT_CONFIG).cryptoCertificatekeypairsList({
ordering: "name",
pageSize: 20,
search: search.trim(),
page,
hasKey: undefined,
});
return {
pagination: certificates.pagination,
options: certificates.results.map(certToSelect),
};
}
export function certificateSelector(instanceMappings?: string[]) {
if (!instanceMappings) {
return [];
}
return async () => {
const pm = new CryptoApi(DEFAULT_CONFIG);
const mappings = await Promise.allSettled(
instanceMappings.map((instanceId) =>
pm.cryptoCertificatekeypairsRetrieve({ kpUuid: instanceId }),
),
);
return mappings
.filter((s) => s.status === "fulfilled")
.map((s) => s.value)
.map(certToSelect);
};
}

View File

@ -1,6 +1,6 @@
import { WithLicenseSummary } from "#elements/mixins/license";
import "@goauthentik/elements/Alert";
import { AKElement } from "@goauthentik/elements/Base";
import { WithLicenseSummary } from "@goauthentik/elements/Interface/licenseSummaryProvider";
import { msg } from "@lit/localize";
import { html, nothing } from "lit";

View File

@ -75,7 +75,7 @@ export const CryptoCertificateSearch = () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const showMessage = (ev: CustomEvent<any>) => {
const detail = ev.detail;
delete detail["target"];
delete detail.target;
document.getElementById("message-pad")!.innerText = `Event: ${JSON.stringify(
detail,
null,

View File

@ -30,11 +30,10 @@ export class CertificateKeyPairForm extends ModelForm<CertificateKeyPair, string
kpUuid: this.instance.pk || "",
patchedCertificateKeyPairRequest: data,
});
} else {
return new CryptoApi(DEFAULT_CONFIG).cryptoCertificatekeypairsCreate({
certificateKeyPairRequest: data as unknown as CertificateKeyPairRequest,
});
}
return new CryptoApi(DEFAULT_CONFIG).cryptoCertificatekeypairsCreate({
certificateKeyPairRequest: data as unknown as CertificateKeyPairRequest,
});
}
renderForm(): TemplateResult {

View File

@ -1,11 +1,11 @@
import { EventGeo, EventUser } from "@goauthentik/admin/events/utils";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EventWithContext } from "@goauthentik/common/events";
import { actionToLabel } from "@goauthentik/common/labels";
import { formatElapsedTime } from "@goauthentik/common/temporal";
import "@goauthentik/components/ak-event-info";
import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/PageHeader";
import { EventGeo, EventUser } from "#admin/events/utils";
import { DEFAULT_CONFIG } from "#common/api/config";
import { EventWithContext } from "#common/events";
import { actionToLabel } from "#common/labels";
import { formatElapsedTime } from "#common/temporal";
import "#components/ak-event-info";
import "#components/ak-page-header";
import { AKElement } from "#elements/Base";
import { msg, str } from "@lit/localize";
import { CSSResult, PropertyValues, TemplateResult, html } from "lit";

View File

@ -51,11 +51,10 @@ export class RuleForm extends ModelForm<NotificationRule, string> {
pbmUuid: this.instance.pk || "",
notificationRuleRequest: data,
});
} else {
return new EventsApi(DEFAULT_CONFIG).eventsRulesCreate({
notificationRuleRequest: data,
});
}
return new EventsApi(DEFAULT_CONFIG).eventsRulesCreate({
notificationRuleRequest: data,
});
}
renderForm(): TemplateResult {

View File

@ -1,5 +1,4 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
import "@goauthentik/elements/forms/Radio";
@ -47,11 +46,10 @@ export class TransportForm extends ModelForm<NotificationTransport, string> {
uuid: this.instance.pk || "",
notificationTransportRequest: data,
});
} else {
return new EventsApi(DEFAULT_CONFIG).eventsTransportsCreate({
notificationTransportRequest: data,
});
}
return new EventsApi(DEFAULT_CONFIG).eventsTransportsCreate({
notificationTransportRequest: data,
});
}
onModeChange(mode: string | undefined): void {
@ -185,7 +183,7 @@ export class TransportForm extends ModelForm<NotificationTransport, string> {
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.sendOnce, false)}
?checked=${this.instance?.sendOnce ?? false}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">

View File

@ -1,11 +1,7 @@
import { CapabilitiesEnum, WithCapabilitiesConfig } from "#elements/mixins/capabilities";
import { DesignationToLabel, LayoutToLabel } from "@goauthentik/admin/flows/utils";
import { AuthenticationEnum } from "@goauthentik/api/dist/models/AuthenticationEnum";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import {
CapabilitiesEnum,
WithCapabilitiesConfig,
} from "@goauthentik/elements/Interface/capabilitiesProvider";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
@ -58,7 +54,7 @@ export class FlowForm extends WithCapabilitiesConfig(ModelForm<Flow, string>) {
}
if (this.can(CapabilitiesEnum.CanSaveMedia)) {
const icon = this.getFormFiles()["background"];
const icon = this.getFormFiles().background;
if (icon || this.clearBackground) {
await new FlowsApi(DEFAULT_CONFIG).flowsInstancesSetBackgroundCreate({
slug: flow.slug,
@ -227,7 +223,7 @@ export class FlowForm extends WithCapabilitiesConfig(ModelForm<Flow, string>) {
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.compatibilityMode, false)}
?checked=${this.instance?.compatibilityMode ?? false}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
@ -407,7 +403,7 @@ export class FlowForm extends WithCapabilitiesConfig(ModelForm<Flow, string>) {
>
<input
type="text"
value="${first(this.instance?.background, "")}"
value="${this.instance?.background ?? ""}"
class="pf-c-form-control"
/>
<p class="pf-c-form__helper-text">

View File

@ -27,7 +27,7 @@ export class FlowImportForm extends Form<Flow> {
}
async send(): Promise<FlowImportResult> {
const file = this.getFormFiles()["flow"];
const file = this.getFormFiles().flow;
if (!file) {
throw new SentryIgnoredError("No form data");
}

View File

@ -1,16 +1,16 @@
import "@goauthentik/admin/flows/BoundStagesList";
import "@goauthentik/admin/flows/FlowDiagram";
import "@goauthentik/admin/flows/FlowForm";
import { DesignationToLabel } from "@goauthentik/admin/flows/utils";
import "@goauthentik/admin/policies/BoundPoliciesList";
import "@goauthentik/admin/rbac/ObjectPermissionsPage";
import { AndNext, DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { isResponseErrorLike } from "@goauthentik/common/errors/network";
import "@goauthentik/components/events/ObjectChangelog";
import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/PageHeader";
import "@goauthentik/elements/Tabs";
import "@goauthentik/elements/buttons/SpinnerButton";
import "#admin/flows/BoundStagesList";
import "#admin/flows/FlowDiagram";
import "#admin/flows/FlowForm";
import { DesignationToLabel } from "#admin/flows/utils";
import "#admin/policies/BoundPoliciesList";
import "#admin/rbac/ObjectPermissionsPage";
import { AndNext, DEFAULT_CONFIG } from "#common/api/config";
import { isResponseErrorLike } from "#common/errors/network";
import "#components/ak-page-header";
import "#components/events/ObjectChangelog";
import { AKElement } from "#elements/Base";
import "#elements/Tabs";
import "#elements/buttons/SpinnerButton/ak-spinner-button";
import { msg } from "@lit/localize";
import { CSSResult, PropertyValues, TemplateResult, css, html } from "lit";

View File

@ -1,5 +1,5 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first, groupBy } from "@goauthentik/common/utils";
import { groupBy } from "@goauthentik/common/utils";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
import "@goauthentik/elements/forms/Radio";
@ -39,9 +39,8 @@ export class StageBindingForm extends ModelForm<FlowStageBinding, string> {
getSuccessMessage(): string {
if (this.instance?.pk) {
return msg("Successfully updated binding.");
} else {
return msg("Successfully created binding.");
}
return msg("Successfully created binding.");
}
send(data: FlowStageBinding): Promise<unknown> {
@ -50,14 +49,13 @@ export class StageBindingForm extends ModelForm<FlowStageBinding, string> {
fsbUuid: this.instance.pk,
patchedFlowStageBindingRequest: data,
});
} else {
if (this.targetPk) {
data.target = this.targetPk;
}
return new FlowsApi(DEFAULT_CONFIG).flowsBindingsCreate({
flowStageBindingRequest: data,
});
}
if (this.targetPk) {
data.target = this.targetPk;
}
return new FlowsApi(DEFAULT_CONFIG).flowsBindingsCreate({
flowStageBindingRequest: data,
});
}
async getOrder(): Promise<number> {
@ -123,7 +121,7 @@ export class StageBindingForm extends ModelForm<FlowStageBinding, string> {
<ak-form-element-horizontal label=${msg("Order")} ?required=${true} name="order">
<input
type="number"
value="${first(this.instance?.order, this.defaultOrder)}"
value="${this.instance?.order ?? this.defaultOrder}"
class="pf-c-form-control"
required
/>
@ -133,7 +131,7 @@ export class StageBindingForm extends ModelForm<FlowStageBinding, string> {
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.evaluateOnPlan, false)}
?checked=${this.instance?.evaluateOnPlan ?? false}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
@ -151,7 +149,7 @@ export class StageBindingForm extends ModelForm<FlowStageBinding, string> {
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.reEvaluatePolicies, true)}
?checked=${this.instance?.reEvaluatePolicies ?? true}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">

View File

@ -1,6 +1,5 @@
import "@goauthentik/admin/groups/MemberSelectModal";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/CodeMirror";
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-provider";
@ -55,12 +54,11 @@ export class GroupForm extends ModelForm<Group, string> {
groupUuid: this.instance.pk,
patchedGroupRequest: data,
});
} else {
data.users = [];
return new CoreApi(DEFAULT_CONFIG).coreGroupsCreate({
groupRequest: data,
});
}
data.users = [];
return new CoreApi(DEFAULT_CONFIG).coreGroupsCreate({
groupRequest: data,
});
}
renderForm(): TemplateResult {
@ -77,7 +75,7 @@ export class GroupForm extends ModelForm<Group, string> {
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.isSuperuser, false)}
?checked=${this.instance?.isSuperuser ?? false}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
@ -150,7 +148,7 @@ export class GroupForm extends ModelForm<Group, string> {
>
<ak-codemirror
mode=${CodeMirrorMode.YAML}
value="${YAML.stringify(first(this.instance?.attributes, {}))}"
value="${YAML.stringify(this.instance?.attributes ?? {})}"
>
</ak-codemirror>
<p class="pf-c-form__helper-text">

View File

@ -3,11 +3,11 @@ import "@goauthentik/admin/groups/RelatedUserList";
import "@goauthentik/admin/rbac/ObjectPermissionsPage";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_REFRESH } from "@goauthentik/common/constants";
import "@goauthentik/components/ak-page-header";
import "@goauthentik/components/ak-status-label";
import "@goauthentik/components/events/ObjectChangelog";
import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/PageHeader";
import "@goauthentik/elements/Tabs";
import "@goauthentik/elements/buttons/ActionButton";
import "@goauthentik/elements/buttons/SpinnerButton";

View File

@ -1,3 +1,5 @@
import { WithBrandConfig } from "#elements/mixins/branding";
import { CapabilitiesEnum, WithCapabilitiesConfig } from "#elements/mixins/capabilities";
import "@goauthentik/admin/users/ServiceAccountForm";
import "@goauthentik/admin/users/UserActiveForm";
import "@goauthentik/admin/users/UserForm";
@ -11,11 +13,6 @@ import { MessageLevel } from "@goauthentik/common/messages";
import { formatElapsedTime } from "@goauthentik/common/temporal";
import { me } from "@goauthentik/common/users";
import "@goauthentik/components/ak-status-label";
import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider";
import {
CapabilitiesEnum,
WithCapabilitiesConfig,
} from "@goauthentik/elements/Interface/capabilitiesProvider";
import "@goauthentik/elements/buttons/ActionButton";
import "@goauthentik/elements/buttons/Dropdown";
import "@goauthentik/elements/forms/DeleteBulkForm";
@ -295,7 +292,7 @@ export class RelatedUserList extends WithBrandConfig(WithCapabilitiesConfig(Tabl
${msg("Set password")}
</button>
</ak-forms-modal>
${this.brand?.flowRecovery
${this.brand.flowRecovery
? html`
<ak-action-button
class="pf-m-secondary"

View File

@ -65,7 +65,7 @@ export class OutpostDeploymentModal extends ModalButton {
</label>
<input class="pf-c-form-control" readonly type="text" value="true" />
</div>
${this.outpost?.type == OutpostTypeEnum.Proxy
${this.outpost?.type === OutpostTypeEnum.Proxy
? html`
<h3>
${msg(

View File

@ -45,9 +45,9 @@ const providerListArgs = (page: number, search = "") => ({
});
const dualSelectPairMaker = (item: ProviderBase): DualSelectPair => {
const label = item.assignedBackchannelApplicationName
? item.assignedBackchannelApplicationName
: item.assignedApplicationName;
const label =
item.assignedBackchannelApplicationName || item.assignedApplicationName || item.name;
return [
`${item.pk}`,
html`<div class="selection-main">${label}</div>
@ -129,11 +129,10 @@ export class OutpostForm extends ModelForm<Outpost, string> {
uuid: this.instance.pk || "",
outpostRequest: data,
});
} else {
return new OutpostsApi(DEFAULT_CONFIG).outpostsInstancesCreate({
outpostRequest: data,
});
}
return new OutpostsApi(DEFAULT_CONFIG).outpostsInstancesCreate({
outpostRequest: data,
});
}
renderForm(): TemplateResult {

View File

@ -1,6 +1,5 @@
import "@goauthentik/admin/common/ak-crypto-certificate-search";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
import "@goauthentik/elements/forms/SearchSelect";
@ -32,11 +31,10 @@ export class ServiceConnectionDockerForm extends ModelForm<DockerServiceConnecti
uuid: this.instance.pk || "",
dockerServiceConnectionRequest: data,
});
} else {
return new OutpostsApi(DEFAULT_CONFIG).outpostsServiceConnectionsDockerCreate({
dockerServiceConnectionRequest: data,
});
}
return new OutpostsApi(DEFAULT_CONFIG).outpostsServiceConnectionsDockerCreate({
dockerServiceConnectionRequest: data,
});
}
renderForm(): TemplateResult {
@ -53,7 +51,7 @@ export class ServiceConnectionDockerForm extends ModelForm<DockerServiceConnecti
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.local, false)}
?checked=${this.instance?.local ?? false}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">

View File

@ -1,5 +1,4 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/CodeMirror";
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/forms/HorizontalFormElement";
@ -36,11 +35,10 @@ export class ServiceConnectionKubernetesForm extends ModelForm<
uuid: this.instance.pk || "",
kubernetesServiceConnectionRequest: data,
});
} else {
return new OutpostsApi(DEFAULT_CONFIG).outpostsServiceConnectionsKubernetesCreate({
kubernetesServiceConnectionRequest: data,
});
}
return new OutpostsApi(DEFAULT_CONFIG).outpostsServiceConnectionsKubernetesCreate({
kubernetesServiceConnectionRequest: data,
});
}
renderForm(): TemplateResult {
@ -57,7 +55,7 @@ export class ServiceConnectionKubernetesForm extends ModelForm<
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.local, false)}
?checked=${this.instance?.local ?? false}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
@ -75,7 +73,7 @@ export class ServiceConnectionKubernetesForm extends ModelForm<
<ak-form-element-horizontal label=${msg("Kubeconfig")} name="kubeconfig">
<ak-codemirror
mode=${CodeMirrorMode.YAML}
value="${YAML.stringify(first(this.instance?.kubeconfig, {}))}"
value="${YAML.stringify(this.instance?.kubeconfig ?? {})}"
>
</ak-codemirror>
<p class="pf-c-form__helper-text">
@ -87,7 +85,7 @@ export class ServiceConnectionKubernetesForm extends ModelForm<
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.verifySsl, true)}
?checked=${this.instance?.verifySsl ?? true}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">

View File

@ -72,9 +72,8 @@ export class BoundPoliciesList extends Table<PolicyBinding> {
return msg(str`Group ${item.groupObj?.name}`);
} else if (item.user) {
return msg(str`User ${item.userObj?.name}`);
} else {
return msg("-");
}
return msg("-");
}
getPolicyUserGroupRow(item: PolicyBinding): TemplateResult {
@ -123,9 +122,8 @@ export class BoundPoliciesList extends Table<PolicyBinding> {
${msg("Edit User")}
</button>
</ak-forms-modal>`;
} else {
return html``;
}
return html``;
}
renderToolbarSelected(): TemplateResult {

View File

@ -3,7 +3,7 @@ import {
PolicyBindingCheckTargetToLabel,
} from "@goauthentik/admin/policies/utils";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first, groupBy } from "@goauthentik/common/utils";
import { groupBy } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-toggle-group";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
@ -72,9 +72,8 @@ export class PolicyBindingForm extends ModelForm<PolicyBinding, string> {
getSuccessMessage(): string {
if (this.instance?.pk) {
return msg("Successfully updated binding.");
} else {
return msg("Successfully created binding.");
}
return msg("Successfully created binding.");
}
static get styles(): CSSResult[] {
@ -111,11 +110,10 @@ export class PolicyBindingForm extends ModelForm<PolicyBinding, string> {
policyBindingUuid: this.instance.pk,
policyBindingRequest: data,
});
} else {
return new PoliciesApi(DEFAULT_CONFIG).policiesBindingsCreate({
policyBindingRequest: data,
});
}
return new PoliciesApi(DEFAULT_CONFIG).policiesBindingsCreate({
policyBindingRequest: data,
});
}
async getOrder(): Promise<number> {
@ -274,7 +272,7 @@ export class PolicyBindingForm extends ModelForm<PolicyBinding, string> {
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.enabled, true)}
?checked=${this.instance?.enabled ?? true}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
@ -289,7 +287,7 @@ export class PolicyBindingForm extends ModelForm<PolicyBinding, string> {
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.negate, false)}
?checked=${this.instance?.negate ?? false}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
@ -305,7 +303,7 @@ export class PolicyBindingForm extends ModelForm<PolicyBinding, string> {
<ak-form-element-horizontal label=${msg("Order")} ?required=${true} name="order">
<input
type="number"
value="${first(this.instance?.order, this.defaultOrder)}"
value="${this.instance?.order ?? this.defaultOrder}"
class="pf-c-form-control"
required
/>
@ -313,7 +311,7 @@ export class PolicyBindingForm extends ModelForm<PolicyBinding, string> {
<ak-form-element-horizontal label=${msg("Timeout")} ?required=${true} name="timeout">
<input
type="number"
value="${first(this.instance?.timeout, 30)}"
value="${this.instance?.timeout ?? 30}"
class="pf-c-form-control"
required
/>

View File

@ -1,5 +1,4 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-status-label";
import "@goauthentik/elements/CodeMirror";
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";
@ -125,7 +124,7 @@ export class PolicyTestForm extends Form<PolicyTestRequest> {
<ak-form-element-horizontal label=${msg("Context")} name="context">
<ak-codemirror
mode=${CodeMirrorMode.YAML}
value=${YAML.stringify(first(this.request?.context, {}))}
value=${YAML.stringify(this.request?.context ?? {})}
>
</ak-codemirror>
<p class="pf-c-form__helper-text">

View File

@ -1,6 +1,5 @@
import { BasePolicyForm } from "@goauthentik/admin/policies/BasePolicyForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
@ -25,11 +24,10 @@ export class DummyPolicyForm extends BasePolicyForm<DummyPolicy> {
policyUuid: this.instance.pk || "",
dummyPolicyRequest: data,
});
} else {
return new PoliciesApi(DEFAULT_CONFIG).policiesDummyCreate({
dummyPolicyRequest: data,
});
}
return new PoliciesApi(DEFAULT_CONFIG).policiesDummyCreate({
dummyPolicyRequest: data,
});
}
renderForm(): TemplateResult {
@ -51,7 +49,7 @@ export class DummyPolicyForm extends BasePolicyForm<DummyPolicy> {
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.executionLogging, false)}
?checked=${this.instance?.executionLogging ?? false}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
@ -74,7 +72,7 @@ export class DummyPolicyForm extends BasePolicyForm<DummyPolicy> {
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.result, false)}
?checked=${this.instance?.result ?? false}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
@ -91,7 +89,7 @@ export class DummyPolicyForm extends BasePolicyForm<DummyPolicy> {
>
<input
type="number"
value="${first(this.instance?.waitMin, 1)}"
value="${this.instance?.waitMin ?? 1}"
class="pf-c-form-control"
required
/>
@ -108,7 +106,7 @@ export class DummyPolicyForm extends BasePolicyForm<DummyPolicy> {
>
<input
type="number"
value="${first(this.instance?.waitMax, 5)}"
value="${this.instance?.waitMax ?? 5}"
class="pf-c-form-control"
required
/>

View File

@ -1,6 +1,5 @@
import { BasePolicyForm } from "@goauthentik/admin/policies/BasePolicyForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/SearchSelect";
@ -37,11 +36,10 @@ export class EventMatcherPolicyForm extends BasePolicyForm<EventMatcherPolicy> {
policyUuid: this.instance.pk || "",
eventMatcherPolicyRequest: data,
});
} else {
return new PoliciesApi(DEFAULT_CONFIG).policiesEventMatcherCreate({
eventMatcherPolicyRequest: data,
});
}
return new PoliciesApi(DEFAULT_CONFIG).policiesEventMatcherCreate({
eventMatcherPolicyRequest: data,
});
}
renderForm(): TemplateResult {
@ -63,7 +61,7 @@ export class EventMatcherPolicyForm extends BasePolicyForm<EventMatcherPolicy> {
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.executionLogging, false)}
?checked=${this.instance?.executionLogging ?? false}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">

View File

@ -1,6 +1,5 @@
import { BasePolicyForm } from "@goauthentik/admin/policies/BasePolicyForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
@ -25,11 +24,10 @@ export class PasswordExpiryPolicyForm extends BasePolicyForm<PasswordExpiryPolic
policyUuid: this.instance.pk || "",
passwordExpiryPolicyRequest: data,
});
} else {
return new PoliciesApi(DEFAULT_CONFIG).policiesPasswordExpiryCreate({
passwordExpiryPolicyRequest: data,
});
}
return new PoliciesApi(DEFAULT_CONFIG).policiesPasswordExpiryCreate({
passwordExpiryPolicyRequest: data,
});
}
renderForm(): TemplateResult {
@ -51,7 +49,7 @@ export class PasswordExpiryPolicyForm extends BasePolicyForm<PasswordExpiryPolic
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.executionLogging, false)}
?checked=${this.instance?.executionLogging ?? false}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
@ -86,7 +84,7 @@ export class PasswordExpiryPolicyForm extends BasePolicyForm<PasswordExpiryPolic
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.denyOnly, false)}
?checked=${this.instance?.denyOnly ?? false}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">

View File

@ -1,7 +1,6 @@
import { BasePolicyForm } from "@goauthentik/admin/policies/BasePolicyForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { docLink } from "@goauthentik/common/global";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/CodeMirror";
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/forms/FormGroup";
@ -28,11 +27,10 @@ export class ExpressionPolicyForm extends BasePolicyForm<ExpressionPolicy> {
policyUuid: this.instance.pk || "",
expressionPolicyRequest: data,
});
} else {
return new PoliciesApi(DEFAULT_CONFIG).policiesExpressionCreate({
expressionPolicyRequest: data,
});
}
return new PoliciesApi(DEFAULT_CONFIG).policiesExpressionCreate({
expressionPolicyRequest: data,
});
}
renderForm(): TemplateResult {
@ -54,7 +52,7 @@ export class ExpressionPolicyForm extends BasePolicyForm<ExpressionPolicy> {
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.executionLogging, false)}
?checked=${this.instance?.executionLogging ?? false}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">

View File

@ -1,6 +1,5 @@
import { BasePolicyForm } from "@goauthentik/admin/policies/BasePolicyForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/ak-dual-select";
import { DataProvision, DualSelectPair } from "@goauthentik/elements/ak-dual-select/types";
import "@goauthentik/elements/forms/FormGroup";
@ -16,7 +15,7 @@ import { DetailedCountry, GeoIPPolicy, PoliciesApi } from "@goauthentik/api";
import { countryCache } from "./CountryCache";
function countryToPair(country: DetailedCountry): DualSelectPair {
return [country.code, country.name];
return [country.code, country.name, country.name];
}
@customElement("ak-policy-geoip-form")
@ -39,11 +38,10 @@ export class GeoIPPolicyForm extends BasePolicyForm<GeoIPPolicy> {
policyUuid: this.instance.pk || "",
geoIPPolicyRequest: data,
});
} else {
return new PoliciesApi(DEFAULT_CONFIG).policiesGeoipCreate({
geoIPPolicyRequest: data,
});
}
return new PoliciesApi(DEFAULT_CONFIG).policiesGeoipCreate({
geoIPPolicyRequest: data,
});
}
renderForm(): TemplateResult {
@ -112,7 +110,7 @@ export class GeoIPPolicyForm extends BasePolicyForm<GeoIPPolicy> {
<input
type="number"
min="1"
value="${first(this.instance?.historyMaxDistanceKm, 100)}"
value="${this.instance?.historyMaxDistanceKm ?? 100}"
class="pf-c-form-control"
/>
<p class="pf-c-form__helper-text">
@ -128,7 +126,7 @@ export class GeoIPPolicyForm extends BasePolicyForm<GeoIPPolicy> {
<input
type="number"
min="1"
value="${first(this.instance?.distanceToleranceKm, 50)}"
value="${this.instance?.distanceToleranceKm ?? 50}"
class="pf-c-form-control"
/>
<p class="pf-c-form__helper-text">
@ -142,7 +140,7 @@ export class GeoIPPolicyForm extends BasePolicyForm<GeoIPPolicy> {
<input
type="number"
min="1"
value="${first(this.instance?.historyLoginCount, 5)}"
value="${this.instance?.historyLoginCount ?? 5}"
class="pf-c-form-control"
/>
<p class="pf-c-form__helper-text">
@ -178,7 +176,7 @@ export class GeoIPPolicyForm extends BasePolicyForm<GeoIPPolicy> {
<input
type="number"
min="1"
value="${first(this.instance?.impossibleToleranceKm, 50)}"
value="${this.instance?.impossibleToleranceKm ?? 50}"
class="pf-c-form-control"
/>
<p class="pf-c-form__helper-text">
@ -211,17 +209,16 @@ export class GeoIPPolicyForm extends BasePolicyForm<GeoIPPolicy> {
.getCountries()
.then((results) => {
if (!search) return results;
return results.filter((result) =>
result.name
.toLowerCase()
.includes(search.toLowerCase()),
);
})
.then((results) => {
return {
options: results.map(countryToPair),
};
});
.then((results) => ({
options: results.map(countryToPair),
}));
}}
.selected=${(this.instance?.countriesObj ?? []).map(countryToPair)}
available-label="${msg("Available Countries")}"

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