core: Format. Fix. WIP.

This commit is contained in:
Teffen Ellis
2025-03-28 00:38:59 +01:00
parent 40ab2bccfd
commit 0e57e06191
311 changed files with 46903 additions and 5734 deletions

View File

@ -10,6 +10,9 @@ insert_final_newline = true
[*.html]
indent_size = 2
[schemas/*.json]
indent_size = 2
[*.{yaml,yml}]
indent_size = 2

View File

@ -3,11 +3,14 @@
## Static Files
**/LICENSE
authentik/stages/**/*
## Build asset directories
/build
coverage
dist
out
.docusaurus
website/docs/developer-docs/api/**/*
## Environment
*.env
@ -20,9 +23,25 @@ out
## Node
node_modules
coverage
## Configs
*.log
*.yaml
*.yml
# Templates
# TODO: Rename affected files to *.template.* or similar.
*.html
*.mdx
*.md
## Import order matters
poly.ts
src/locale-codes.ts
src/locales/
# Storybook
storybook-static/
.storybook/css-import-maps*

View File

@ -17,6 +17,6 @@
"ms-python.vscode-pylance",
"redhat.vscode-yaml",
"Tobermory.es6-string-html",
"unifiedjs.vscode-mdx",
"unifiedjs.vscode-mdx"
]
}

57
.vscode/settings.json vendored
View File

@ -16,7 +16,7 @@
],
"typescript.preferences.importModuleSpecifier": "non-relative",
"typescript.preferences.importModuleSpecifierEnding": "index",
"typescript.tsdk": "./web/node_modules/typescript/lib",
"typescript.tsdk": "./node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true,
"yaml.schemas": {
"./blueprints/schema.json": "blueprints/**/*.yaml"
@ -30,7 +30,56 @@
}
],
"go.testFlags": ["-count=1"],
"github-actions.workflows.pinned.workflows": [
".github/workflows/ci-main.yml"
]
"github-actions.workflows.pinned.workflows": [".github/workflows/ci-main.yml"],
"eslint.useFlatConfig": true,
"explorer.fileNesting.enabled": true,
"explorer.fileNesting.patterns": {
"*.cjs": "*.d.cts",
"package.json": "package-lock.json, yarn.lock, .yarnrc, .yarnrc.yml, .yarn, .nvmrc, .node-version",
"tsconfig.json": "tsconfig.*.json, jsconfig.json"
},
"search.exclude": {
"**/node_modules": true,
"**/*.code-search": true,
"**/dist": true,
"**/out": true,
"**/package-lock.json": true
},
"[css]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[markdown]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[shellscript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"editor.codeActionsOnSave": {
"source.removeUnusedImports": "explicit"
},
// We use Prettier for formatting, but specifying these settings
// will ensure that VS Code's IntelliSense doesn't autocomplete unformatted code.
"javascript.format.semicolons": "insert",
"typescript.format.semicolons": "insert",
"javascript.preferences.quoteStyle": "double",
"typescript.preferences.quoteStyle": "double"
}

40
.vscode/tasks.json vendored
View File

@ -4,12 +4,7 @@
{
"label": "authentik/core: make",
"command": "uv",
"args": [
"run",
"make",
"lint-fix",
"lint"
],
"args": ["run", "make", "lint-fix", "lint"],
"presentation": {
"panel": "new"
},
@ -18,11 +13,7 @@
{
"label": "authentik/core: run",
"command": "uv",
"args": [
"run",
"ak",
"server"
],
"args": ["run", "ak", "server"],
"group": "build",
"presentation": {
"panel": "dedicated",
@ -32,17 +23,13 @@
{
"label": "authentik/web: make",
"command": "make",
"args": [
"web"
],
"args": ["web"],
"group": "build"
},
{
"label": "authentik/web: watch",
"command": "make",
"args": [
"web-watch"
],
"args": ["web-watch"],
"group": "build",
"presentation": {
"panel": "dedicated",
@ -52,26 +39,19 @@
{
"label": "authentik: install",
"command": "make",
"args": [
"install",
"-j4"
],
"args": ["install", "-j4"],
"group": "build"
},
{
"label": "authentik/website: make",
"command": "make",
"args": [
"website"
],
"args": ["website"],
"group": "build"
},
{
"label": "authentik/website: watch",
"command": "make",
"args": [
"website-watch"
],
"args": ["website-watch"],
"group": "build",
"presentation": {
"panel": "dedicated",
@ -81,11 +61,7 @@
{
"label": "authentik/api: generate",
"command": "uv",
"args": [
"run",
"make",
"gen"
],
"args": ["run", "make", "gen"],
"group": "build"
}
]

View File

@ -133,16 +133,14 @@ gen-client-ts: gen-clean-ts ## Build and install the authentik API for Typescri
--rm -v ${PWD}:/local \
--user ${UID}:${GID} \
docker.io/openapitools/openapi-generator-cli:v7.11.0 generate \
-i /local/schema.yml \
-g typescript-fetch \
-o /local/${GEN_API_TS} \
-c /local/scripts/api-ts-config.yaml \
--input-spec /local/schema.yml \
--generator-name typescript-fetch \
--output /local/${GEN_API_TS} \
--config /local/scripts/api-ts-config.yaml \
--additional-properties=npmVersion=${NPM_VERSION} \
--git-repo-id authentik \
--git-user-id goauthentik
mkdir -p web/node_modules/@goauthentik/api
cd ./${GEN_API_TS} && npm i
\cp -rf ./${GEN_API_TS}/* web/node_modules/@goauthentik/api
npm install
gen-client-py: gen-clean-py ## Build and install the authentik API for Python
docker run \

View File

@ -323,12 +323,7 @@
"multiValued": false,
"description": "Indicates whether or not an attribute is modifiable.",
"required": false,
"canonicalValues": [
"readOnly",
"readWrite",
"immutable",
"writeOnly"
],
"canonicalValues": ["readOnly", "readWrite", "immutable", "writeOnly"],
"caseExact": false,
"mutability": "readOnly",
"returned": "default",
@ -340,12 +335,7 @@
"multiValued": false,
"description": "Indicates when an attribute is returned in a response (e.g., to a query).",
"required": false,
"canonicalValues": [
"always",
"never",
"default",
"request"
],
"canonicalValues": ["always", "never", "default", "request"],
"caseExact": false,
"mutability": "readOnly",
"returned": "default",
@ -369,12 +359,7 @@
"multiValued": true,
"description": "Used only with an attribute of type 'reference'. Specifies a SCIM resourceType that a reference attribute MAY refer to, e.g., 'User'.",
"required": false,
"canonicalValues": [
"resource",
"external",
"uri",
"url"
],
"canonicalValues": ["resource", "external", "uri", "url"],
"caseExact": false,
"mutability": "readOnly",
"returned": "default",

File diff suppressed because it is too large Load Diff

10
eslint.config.mjs Normal file
View File

@ -0,0 +1,10 @@
import { createESLintPackageConfig } from "@goauthentik/eslint-config";
// @ts-check
/**
* ESLint configuration for authentik's monorepo.
*/
const ESLintConfig = createESLintPackageConfig();
export default ESLintConfig;

View File

@ -1,16 +1,16 @@
{
"name": "@goauthentik/lifecycle-aws",
"version": "0.0.0",
"private": true,
"license": "MIT",
"scripts": {
"aws-cfn": "cross-env CI=false cdk synth --version-reporting=false > template.yaml"
},
"engines": {
"node": ">=20"
},
"devDependencies": {
"aws-cdk": "^2.1005.0",
"cross-env": "^7.0.3"
},
"private": true,
"license": "MIT",
"engines": {
"node": ">=20"
}
}

43195
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,38 @@
{
"name": "@goauthentik/authentik",
"name": "@goauthentik/universe",
"version": "2025.2.2",
"private": true
"description": "Monorepo for authentik.",
"private": true,
"scripts": {
"compile": "NODE_OPTIONS=\"--max-old-space-size=3000\" tsc -b",
"compile:clean": "node ./sdk/out/scripts/clean.js",
"lint": "run-s lint:prettier:check lint:eslint:check",
"lint:eslint:check": "eslint .",
"lint:eslint:fix": "eslint --fix .",
"lint:fix": "run-s lint:prettier:fix lint:eslint:fix",
"lint:prettier": "eslint .",
"lint:prettier:check": "prettier --cache --check -u .",
"lint:prettier:fix": "prettier --cache --write -u ."
},
"dependencies": {
"@eslint/js": "^9.11.1",
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
"@typescript-eslint/eslint-plugin": "^8.28.0",
"@typescript-eslint/parser": "^8.28.0",
"eslint": "^9.23.0",
"eslint-plugin-lit": "^2.0.0",
"eslint-plugin-wc": "^3.0.0",
"npm-run-all": "^4.1.5",
"prettier": "^3.5.3",
"typescript": "^5.8.2",
"typescript-eslint": "^8.29.0",
"zx": "^8.4.1"
},
"workspaces": [
"gen-ts-api",
"web",
"website",
"packages/*"
],
"prettier": "@goauthentik/prettier-config"
}

View File

@ -0,0 +1,11 @@
/**
* @file TypeScript type definitions for eslint-plugin-react-hooks
*/
declare module "eslint-plugin-react-hooks" {
import { ESLint } from "eslint";
// We have to do this because ESLint aliases the namespace and class simultaneously.
type PluginInstance = ESLint.Plugin;
const Plugin: PluginInstance;
export default Plugin;
}

View File

@ -0,0 +1,11 @@
/**
* @file TypeScript type definitions for eslint-plugin-react
*/
declare module "eslint-plugin-react" {
import { ESLint } from "eslint";
// We have to do this because ESLint aliases the namespace and class simultaneously.
type PluginInstance = ESLint.Plugin;
const Plugin: PluginInstance;
export default Plugin;
}

View File

@ -1,8 +1,9 @@
import eslint from "@eslint/js";
import { javaScriptConfig } from "@goauthentik/eslint-config/javascript-config";
import { reactConfig } from "@goauthentik/eslint-config/react-config";
import { typescriptConfig } from "@goauthentik/eslint-config/typescript-config";
import litconf from "eslint-plugin-lit";
import wcconf from "eslint-plugin-wc";
import * as litconf from "eslint-plugin-lit";
import * as wcconf from "eslint-plugin-wc";
import tseslint from "typescript-eslint";
// @ts-check
@ -10,7 +11,6 @@ import tseslint from "typescript-eslint";
/**
* @typedef ESLintPackageConfigOptions Options for creating package ESLint configuration.
* @property {string[]} [ignorePatterns] Override ignore patterns for ESLint.
* @property {import("typescript-eslint").ConfigWithExtends} [overrides] Additional ESLint rules
*/
/**
@ -19,9 +19,17 @@ import tseslint from "typescript-eslint";
export const DefaultIgnorePatterns = [
// ---
"**/*.md",
"**/.yarn",
"**/out",
"**/dist",
"**/.wireit",
"website/build/**",
"website/.docusaurus/**",
"**/node_modules",
"**/coverage",
"**/storybook-static",
"**/locale-codes.ts",
"**/src/locales",
"**/gen-ts-api",
];
/**
@ -31,24 +39,34 @@ export const DefaultIgnorePatterns = [
*
* @returns The ESLint configuration object.
*/
export function createESLintPackageConfig({
ignorePatterns = DefaultIgnorePatterns,
overrides = {},
} = {}) {
export function createESLintPackageConfig({ ignorePatterns = DefaultIgnorePatterns } = {}) {
return tseslint.config(
{
ignores: ignorePatterns,
},
eslint.configs.recommended,
...tseslint.configs.recommended,
eslint.configs.recommended,
javaScriptConfig,
wcconf.configs["flat/recommended"],
litconf.configs["flat/recommended"],
...reactConfig,
...tseslint.configs.recommended,
...typescriptConfig,
overrides,
...reactConfig,
{
rules: {
"no-console": "off",
},
files: [
// ---
"**/scripts/**/*",
"**/test/**/*",
"**/tests/**/*",
],
},
);
}

View File

@ -0,0 +1,143 @@
// @ts-check
import tseslint from "typescript-eslint";
const MAX_DEPTH = 4;
const MAX_NESTED_CALLBACKS = 4;
const MAX_PARAMS = 5;
/**
* ESLint configuration for JavaScript authentik projects.
*/
export const javaScriptConfig = tseslint.config({
rules: {
// TODO: Clean up before enabling.
"accessor-pairs": "off",
"array-callback-return": "error",
"block-scoped-var": "error",
"consistent-return": ["error", { treatUndefinedAsUnspecified: false }],
"consistent-this": ["error", "that"],
"curly": "off",
"dot-notation": [
"error",
{
allowKeywords: true,
},
],
"eqeqeq": "error",
"func-names": ["error", "as-needed"],
"guard-for-in": "error",
"max-depth": ["error", MAX_DEPTH],
"max-nested-callbacks": ["error", MAX_NESTED_CALLBACKS],
"max-params": ["error", MAX_PARAMS],
// TODO: Clean up before enabling.
// "new-cap": "error",
"no-alert": "error",
"no-array-constructor": "error",
"no-bitwise": [
"error",
{
allow: ["~"],
int32Hint: true,
},
],
"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", { allow: ["constructors"] }],
"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-label-var": "error",
"no-lone-blocks": "error",
"no-lonely-if": "error",
"no-loop-func": "error",
"no-multi-str": "error",
// TODO: Clean up before enabling.
"no-negated-condition": "off",
"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", { props: false }],
"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",
// TODO: Clean up before enabling.
// "no-shadow": "error",
"no-shadow-restricted-names": "error",
"no-sparse-arrays": "error",
"no-this-before-super": "error",
"no-throw-literal": "error",
"no-trailing-spaces": "off", // Handled by Prettier.
"no-undef": "off",
"no-undef-init": "off",
"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-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",
},
});
export default javaScriptConfig;

View File

@ -14,13 +14,15 @@
"import": "./react-config.js",
"types": "./out/react-config.d.ts"
},
"./javascript-config": {
"import": "./javascript-config.js",
"types": "./out/javascript-config.d.ts"
},
"./typescript-config": {
"import": "./typescript-config.js",
"types": "./out/typescript-config.d.ts"
}
},
"types": "./out/index.d.ts",
"prettier": "@goauthentik/prettier-config",
"dependencies": {
"eslint": "^9.23.0",
"eslint-plugin-import": "^2.31.0",
@ -30,13 +32,12 @@
"devDependencies": {
"@goauthentik/tsconfig": "1.0.0",
"@types/eslint": "^9.6.1",
"@types/eslint__js": "^9.14.0",
"typescript": "^5.8.2",
"typescript-eslint": "^8.27.0"
"typescript-eslint": "^8.29.0"
},
"peerDependencies": {
"typescript": "^5.8.2",
"typescript-eslint": "^8.27.0"
"typescript-eslint": "^8.29.0"
},
"optionalDependencies": {
"react": "^18.3.1"
@ -44,6 +45,8 @@
"engines": {
"node": ">=20.11"
},
"types": "./out/index.d.ts",
"prettier": "@goauthentik/prettier-config",
"publishConfig": {
"access": "public"
}

View File

@ -1,12 +1,11 @@
import reactPlugin from "eslint-plugin-react"
import hooksPlugin from "eslint-plugin-react-hooks"
import tseslint from "typescript-eslint"
import reactPlugin from "eslint-plugin-react";
import hooksPlugin from "eslint-plugin-react-hooks";
import tseslint from "typescript-eslint";
/**
* ESLint configuration for React authentik projects.
*/
export const reactConfig = tseslint.config(
{
export const reactConfig = tseslint.config({
settings: {
react: {
version: "detect",
@ -14,9 +13,7 @@ export const reactConfig = tseslint.config(
},
plugins: {
// @ts-ignore Fixup plugin rules.
react: reactPlugin,
// @ts-ignore Fixup plugin
"react": reactPlugin,
"react-hooks": hooksPlugin,
},
@ -32,7 +29,6 @@ export const reactConfig = tseslint.config(
"react/prop-types": "off",
"react/react-in-jsx-scope": "off",
},
}
)
});
export default reactConfig
export default reactConfig;

View File

@ -1,6 +1,7 @@
{
"extends": "@goauthentik/tsconfig",
"compilerOptions": {
"baseUrl": ".",
"checkJs": true,
"emitDeclarationOnly": true
}

View File

@ -7,54 +7,28 @@ import tseslint from "typescript-eslint";
export const typescriptConfig = tseslint.config({
rules: {
"@typescript-eslint/ban-ts-comment": [
"warn",
"error",
{
"ts-ignore": "allow-with-description",
},
],
"@typescript-eslint/ban-types": "off",
"@typescript-eslint/no-empty-interface": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-extra-semi": "off",
"@typescript-eslint/no-misused-new": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-shadow": [
"warn",
{
ignoreFunctionTypeParameterNameValueShadow: true,
ignoreTypeValueShadow: true,
"ts-expect-error": "allow-with-description",
"ts-ignore": true,
"ts-nocheck": "allow-with-description",
"ts-check": false,
"minimumDescriptionLength": 5,
},
],
"no-use-before-define": "off",
"@typescript-eslint/no-use-before-define": "error",
"no-invalid-this": "off",
"no-unused-vars": "off",
"@typescript-eslint/no-namespace": "off",
"@typescript-eslint/no-unused-vars": [
"warn",
{
args: "all",
argsIgnorePattern: "^_",
caughtErrors: "all",
varsIgnorePattern: "^_",
caughtErrorsIgnorePattern: "^_",
destructuredArrayIgnorePattern: "^_",
// Ignore all variables, since Prettier takes care of this.
varsIgnorePattern: "^\\w",
ignoreRestSiblings: true,
},
],
"@typescript-eslint/no-var-requires": "off",
"eqeqeq": ["error", "always", { null: "ignore" }],
"no-shadow": "off",
"no-extra-semi": "off",
"no-undef": "off",
"no-unused-vars": "off",
"object-shorthand": [
"warn",
"always",
{
avoidQuotes: true,
ignoreConstructors: true,
avoidExplicitReturnArrows: false,
},
],
"prefer-const": "warn",
},
});

View File

@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2024 Authentik Security, Inc.
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,

View File

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

View File

@ -0,0 +1,17 @@
/**
* @file Constants for JavaScript and TypeScript files.
*
*/
/**
* 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.
*
*/
export const NodeEnvironment = /** @type {'development' | 'production'} */ (
process.env.NODE_ENV || "development"
);

View File

@ -0,0 +1,4 @@
export * from "./paths.js";
export * from "./constants.js";
export * from "./version.js";
export * from "./scripting.js";

View File

@ -0,0 +1,19 @@
{
"name": "@goauthentik/monorepo",
"version": "1.0.0",
"description": "Utilities for the authentik monorepo.",
"private": true,
"license": "MIT",
"type": "module",
"exports": {
"./package.json": "./package.json",
".": {
"import": "./index.js",
"types": "./out/index.d.ts"
}
},
"types": "./out/index.d.ts",
"engines": {
"node": ">=20.11"
}
}

View File

@ -0,0 +1,30 @@
import { createRequire } from "node:module";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
/**
* @typedef {'~authentik'} MonoRepoRoot
*/
/**
* The root of the authentik monorepo.
*/
export const MonoRepoRoot = /** @type {MonoRepoRoot} */ (resolve(__dirname, "..", ".."));
const require = createRequire(import.meta.url);
/**
* Resolve a package name to its location in the monorepo to the single node_modules directory.
* @param {string} packageName
* @returns {string} The resolved path to the package.
* @throws {Error} If the package cannot be resolved.
*/
export function resolvePackage(packageName) {
const packageJSONPath = require.resolve(join(packageName, "package.json"), {
paths: [MonoRepoRoot],
});
return dirname(packageJSONPath);
}

View File

@ -0,0 +1,40 @@
import { createRequire } from "module";
import * as path from "path";
import * as process from "process";
import { fileURLToPath } from "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.
*/
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

View File

@ -0,0 +1,9 @@
{
"extends": "@goauthentik/tsconfig",
"compilerOptions": {
"resolveJsonModule": true,
"baseUrl": ".",
"checkJs": true,
"emitDeclarationOnly": true
}
}

View File

@ -0,0 +1,45 @@
import { execSync } from "node:child_process";
import PackageJSON from "../../package.json" with { type: "json" };
import { MonoRepoRoot } from "./paths.js";
/**
* The current version of authentik in SemVer format.
*
*/
export const AuthentikVersion = /**@type {`${number}.${number}.${number}`} */ (PackageJSON.version);
/**
* Reads the last commit hash from the current git repository.
*/
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.
*
* @see {@link "authentik\_\_init\_\_.py"}
*/
export function readBuildIdentifier() {
const { GIT_BUILD_HASH = "d72def036820985a909266e8167ccb8087c7ce32" } = process.env;
if (!GIT_BUILD_HASH) return AuthentikVersion;
return [AuthentikVersion, GIT_BUILD_HASH].join("+");
}

View File

@ -1,7 +1,17 @@
/**
* @file Prettier configuration for authentik.
*
* @import { Config as PrettierConfig } from "prettier";
* @import { PluginConfig as SortPluginConfig } from "@trivago/prettier-plugin-sort-imports";
*
* @typedef {object} PackageJSONPluginConfig
* @property {string[]} [packageSortOrder] Custom ordering array.
*/
/**
* authentik Prettier configuration.
*
* @type {import("prettier").Config}
* @type {PrettierConfig & SortPluginConfig & PackageJSONPluginConfig}
* @internal
*/
export const AuthentikPrettierConfig = {
@ -27,6 +37,12 @@ export const AuthentikPrettierConfig = {
importOrderSortSpecifiers: true,
importOrderParserPlugins: ["typescript", "jsx", "classProperties", "decorators-legacy"],
overrides: [
{
files: "schemas/**/*.json",
options: {
tabWidth: 2,
},
},
{
files: "tsconfig.json",
options: {
@ -41,9 +57,22 @@ export const AuthentikPrettierConfig = {
"name",
"version",
"description",
"license",
"private",
"author",
"authors",
"scripts",
"devDependencies",
"main",
"type",
"exports",
"imports",
"dependencies",
"devDependencies",
"peerDependencies",
"optionalDependencies",
"wireit",
"resolutions",
"engines",
],
},
},

View File

@ -1,4 +1,5 @@
import { format } from "prettier";
import { AuthentikPrettierConfig } from "./config.js";
/**

View File

@ -2,8 +2,8 @@
"name": "@goauthentik/prettier-config",
"version": "1.0.0",
"description": "authentik's Prettier config",
"type": "module",
"license": "MIT",
"type": "module",
"exports": {
"./package.json": "./package.json",
".": {
@ -13,9 +13,9 @@
},
"types": "./out/index.d.ts",
"peerDependencies": {
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"prettier-plugin-organize-imports": "^4.1.0",
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
"prettier": "^3.5.3",
"prettier-plugin-organize-imports": "^4.1.0",
"prettier-plugin-packagejson": "^2.5.10"
},
"engines": {

View File

@ -1,6 +1,7 @@
{
"extends": "@goauthentik/tsconfig",
"compilerOptions": {
"baseUrl": ".",
"checkJs": true,
"emitDeclarationOnly": true
}

View File

@ -1,66 +0,0 @@
{
"name": "@goauthentik/web-sfe",
"version": "0.0.0",
"private": true,
"license": "MIT",
"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"
},
"dependencies": {
"@goauthentik/api": "^2024.6.0-1719577139",
"base64-js": "^1.5.1",
"bootstrap": "^4.6.1",
"formdata-polyfill": "^4.0.10",
"jquery": "^3.7.1",
"weakmap-polyfill": "^2.0.4"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^28.0.0",
"@rollup/plugin-node-resolve": "^15.3.0",
"@rollup/plugin-swc": "^0.4.0",
"@swc/cli": "^0.4.0",
"@swc/core": "^1.7.28",
"@types/jquery": "^3.5.31",
"lockfile-lint": "^4.14.0",
"rollup": "^4.23.0",
"rollup-plugin-copy": "^3.5.0",
"wireit": "^0.14.9"
},
"optionalDependencies": {
"@swc/core": "^1.7.28",
"@swc/core-darwin-arm64": "^1.6.13",
"@swc/core-darwin-x64": "^1.6.13",
"@swc/core-linux-arm-gnueabihf": "^1.6.13",
"@swc/core-linux-arm64-gnu": "^1.6.13",
"@swc/core-linux-arm64-musl": "^1.6.13",
"@swc/core-linux-x64-gnu": "^1.6.13",
"@swc/core-linux-x64-musl": "^1.6.13",
"@swc/core-win32-arm64-msvc": "^1.6.13",
"@swc/core-win32-ia32-msvc": "^1.6.13",
"@swc/core-win32-x64-msvc": "^1.6.13"
},
"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

@ -28,10 +28,7 @@
"type": {
"description": "A label indicating the type of resource, e.g., 'User' or 'Group'.",
"type": "string",
"enum": [
"User",
"Group"
],
"enum": ["User", "Group"],
"readOnly": true
}
},
@ -39,7 +36,5 @@
}
}
},
"required": [
"displayName"
]
"required": ["displayName"]
}

View File

@ -51,10 +51,7 @@
"readOnly": true
}
},
"required": [
"schema",
"required"
],
"required": ["schema", "required"],
"additionalProperties": true
}
],
@ -62,11 +59,6 @@
"readOnly": true
}
},
"required": [
"name",
"endpoint",
"schema",
"schemaExtensions"
],
"required": ["name", "endpoint", "schema", "schemaExtensions"],
"additionalProperties": true
}

View File

@ -21,9 +21,7 @@
"readOnly": true
}
},
"required": [
"supported"
],
"required": ["supported"],
"readOnly": true
},
"bulk": {
@ -36,9 +34,7 @@
"readOnly": true
}
},
"required": [
"supported"
],
"required": ["supported"],
"readOnly": true
},
"filter": {
@ -56,9 +52,7 @@
"readOnly": true
}
},
"required": [
"supported"
],
"required": ["supported"],
"readOnly": true
},
"changePassword": {
@ -71,9 +65,7 @@
"readOnly": true
}
},
"required": [
"supported"
],
"required": ["supported"],
"readOnly": true
},
"sort": {
@ -86,9 +78,7 @@
"readOnly": true
}
},
"required": [
"supported"
],
"required": ["supported"],
"readOnly": true
},
"authenticationSchemes": {
@ -119,21 +109,11 @@
"readOnly": true
}
},
"required": [
"name",
"description"
],
"required": ["name", "description"],
"readOnly": true
},
"readOnly": true
}
},
"required": [
"patch",
"bulk",
"filter",
"changePassword",
"sort",
"authenticationSchemes"
]
"required": ["patch", "bulk", "filter", "changePassword", "sort", "authenticationSchemes"]
}

View File

@ -99,11 +99,7 @@
"type": {
"description": "A label indicating the attribute's function, e.g., 'work' or 'home'.",
"type": "string",
"enum": [
"work",
"home",
"other"
]
"enum": ["work", "home", "other"]
},
"primary": {
"description": "A Boolean value indicating the 'primary' or preferred attribute value for this attribute, e.g., the preferred mailing address or primary email address. The primary attribute value 'true' MUST appear no more than once.",
@ -129,14 +125,7 @@
"type": {
"description": "A label indicating the attribute's function, e.g., 'work', 'home', 'mobile'.",
"type": "string",
"enum": [
"work",
"home",
"mobile",
"fax",
"pager",
"other"
]
"enum": ["work", "home", "mobile", "fax", "pager", "other"]
},
"primary": {
"description": "A Boolean value indicating the 'primary' or preferred attribute value for this attribute, e.g., the preferred phone number or primary phone number. The primary attribute value 'true' MUST appear no more than once.",
@ -162,16 +151,7 @@
"type": {
"description": "A label indicating the attribute's function, e.g., 'aim', 'gtalk', 'xmpp'.",
"type": "string",
"enum": [
"aim",
"gtalk",
"icq",
"xmpp",
"msn",
"skype",
"qq",
"yahoo"
]
"enum": ["aim", "gtalk", "icq", "xmpp", "msn", "skype", "qq", "yahoo"]
},
"primary": {
"description": "A Boolean value indicating the 'primary' or preferred attribute value for this attribute, e.g., the preferred messenger or primary messenger. The primary attribute value 'true' MUST appear no more than once.",
@ -198,10 +178,7 @@
"type": {
"description": "A label indicating the attribute's function, i.e., 'photo' or 'thumbnail'.",
"type": "string",
"enum": [
"photo",
"thumbnail"
]
"enum": ["photo", "thumbnail"]
},
"primary": {
"description": "A Boolean value indicating the 'primary' or preferred attribute value for this attribute, e.g., the preferred photo or thumbnail. The primary attribute value 'true' MUST appear no more than once.",
@ -243,11 +220,7 @@
"type": {
"description": "A label indicating the attribute's function, e.g., 'work' or 'home'.",
"type": "string",
"enum": [
"work",
"home",
"other"
]
"enum": ["work", "home", "other"]
}
}
}
@ -277,10 +250,7 @@
"type": {
"description": "A label indicating the attribute's function, e.g., 'direct' or 'indirect'.",
"type": "string",
"enum": [
"direct",
"indirect"
],
"enum": ["direct", "indirect"],
"readOnly": true
}
},
@ -366,7 +336,5 @@
}
}
},
"required": [
"userName"
]
"required": ["userName"]
}

View File

@ -19,9 +19,8 @@
"files": [],
"references": [
// Note that references are in the order we want them to be built.
{ "path": "./packages/tsconfig" },
{ "path": "./packages/prettier-config" },
{ "path": "./packages/eslint-config" },
{ "path": "./packages/sfe" }
{ "path": "./web" }
]
}

View File

@ -1,16 +0,0 @@
# don't ever lint node_modules
node_modules
# don't lint build output (make sure it's set to your correct build folder name)
dist
# 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,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", "jsx", "classProperties", "decorators-legacy"]
}

View File

@ -1,13 +1,16 @@
import replace from "@rollup/plugin-replace";
import type { StorybookConfig } from "@storybook/web-components-vite";
import { cwd } from "process";
import { cwd } from "node:process";
import modify from "rollup-plugin-modify";
import postcssLit from "rollup-plugin-postcss-lit";
import { mergeConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
export const isProdBuild = process.env.NODE_ENV === "production";
export const apiBasePath = process.env.AK_API_BASE_PATH || "";
const NODE_ENV = process.env.NODE_ENV || "development";
const AK_API_BASE_PATH = process.env.AK_API_BASE_PATH || "";
const importInlinePatterns = [
'import AKGlobal from "(\\.\\./)*common/styles/authentik\\.css',
'import AKGlobal from "@goauthentik/common/styles/authentik\\.css',
@ -53,8 +56,14 @@ const config: StorybookConfig = {
autodocs: "tag",
},
async viteFinal(config) {
return {
...config,
return mergeConfig(config, {
define: {
"process.env.NODE_ENV": JSON.stringify(NODE_ENV),
"process.env.CWD": JSON.stringify(cwd()),
"process.env.AK_API_BASE_PATH": JSON.stringify(AK_API_BASE_PATH),
"process.env.WATCHER_URL": "",
},
plugins: [
modify({
find: importInlineRegexp,
@ -62,19 +71,10 @@ const config: StorybookConfig = {
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(),
],
};
});
},
};

View File

@ -1,90 +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/",
// 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",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
caughtErrorsIgnorePattern: "^_",
},
],
},
},
];

View File

@ -1,138 +1,9 @@
{
"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.2-1742585853",
"@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.7",
"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",
"@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": "^4.3.0",
"@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",
"find-free-ports": "^3.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"
},
"private": true,
"scripts": {
"build": "wireit",
"build-locales": "wireit",
"build-locales:build": "wireit",
"build-proxy": "wireit",
"build:sfe": "wireit",
"esbuild:watch": "node scripts/build-web.mjs --watch",
@ -160,7 +31,138 @@
"tsc": "wireit",
"watch": "run-s build-locales esbuild:watch"
},
"devDependencies": {
"@goauthentik/monorepo": "1.0.0",
"@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",
"@swc/helpers": "^0.5.15",
"@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.2",
"@types/mocha": "^10.0.8",
"@types/node": "^22.7.4",
"@types/react": "^18.3.13",
"@types/react-dom": "^18.3.0",
"@wdio/browser-runner": "9.12.1",
"@wdio/cli": "9.12.1",
"@wdio/spec-reporter": "^9.11.0",
"chromedriver": "^134.0.5",
"esbuild": "^0.25.0",
"esbuild-plugin-copy": "^2.1.1",
"esbuild-plugin-es5": "^2.1.1",
"esbuild-plugin-polyfill-node": "^0.3.0",
"esbuild-plugins-node-modules-polyfill": "^1.7.0",
"find-free-ports": "^3.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",
"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",
"vite-plugin-lit-css": "^2.0.0",
"vite-tsconfig-paths": "^5.0.1",
"wireit": "^0.14.9",
"zx": "^8.4.1"
},
"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.2-1742585853",
"@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",
"bootstrap": "^4.6.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",
"formdata-polyfill": "^4.0.10",
"fuse.js": "^7.0.0",
"guacamole-common-js": "^1.5.0",
"hastscript": "^9.0.1",
"jquery": "^3.7.1",
"lit": "^3.2.0",
"md-front-matter": "^1.0.4",
"mermaid": "^11.4.1",
"rapidoc": "^9.3.7",
"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",
"weakmap-polyfill": "^2.0.4",
"webcomponent-qr-code": "^1.2.0",
"yaml": "^2.5.1"
},
"private": true,
"license": "MIT",
"type": "module",
"exports": {
"./package.json": "./package.json",
"./paths": "./paths.js",
"./scripts/*": "./scripts/*.mjs"
},
"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"
},
"engines": {
"node": ">=20"
},
"wireit": {
"build": {
"#comment": [
@ -217,12 +219,6 @@
"build-locales"
]
},
"build-locales:build": {
"command": "lit-localize build"
},
"build-locales:repair": {
"command": "prettier --write ./src/locale-codes.ts"
},
"build-locales": {
"command": "node scripts/build-locales.mjs",
"files": [
@ -376,9 +372,5 @@
"lint:types"
]
}
},
"workspaces": [
".",
"./packages/*"
]
}
}

View File

@ -1,3 +1,6 @@
/**
* @file Simple Flow Executor entry point.
*/
import { fromByteArray } from "base64-js";
import "formdata-polyfill";
import $ from "jquery";
@ -33,82 +36,6 @@ function ak(): GlobalAuthentik {
).authentik;
}
class SimpleFlowExecutor {
challenge?: ChallengeTypes;
flowSlug: string;
container: HTMLDivElement;
constructor(container: HTMLDivElement) {
this.flowSlug = window.location.pathname.split("/")[3];
this.container = container;
}
get apiURL() {
return `${ak().api.base}api/v3/flows/executor/${this.flowSlug}/?query=${encodeURIComponent(window.location.search.substring(1))}`;
}
start() {
$.ajax({
type: "GET",
url: this.apiURL,
success: (data) => {
this.challenge = ChallengeTypesFromJSON(data);
this.renderChallenge();
},
});
}
submit(data: { [key: string]: unknown } | FormData) {
$("button[type=submit]").addClass("disabled")
.html(`<span class="spinner-border spinner-border-sm" aria-hidden="true"></span>
<span role="status">Loading...</span>`);
let finalData: { [key: string]: unknown } = {};
if (data instanceof FormData) {
finalData = {};
data.forEach((value, key) => {
finalData[key] = value;
});
} else {
finalData = data;
}
$.ajax({
type: "POST",
url: this.apiURL,
data: JSON.stringify(finalData),
success: (data) => {
this.challenge = ChallengeTypesFromJSON(data);
this.renderChallenge();
},
contentType: "application/json",
dataType: "json",
});
}
renderChallenge() {
switch (this.challenge?.component) {
case "ak-stage-identification":
new IdentificationStage(this, this.challenge).render();
return;
case "ak-stage-password":
new PasswordStage(this, this.challenge).render();
return;
case "xak-flow-redirect":
new RedirectStage(this, this.challenge).render();
return;
case "ak-stage-autosubmit":
new AutosubmitStage(this, this.challenge).render();
return;
case "ak-stage-authenticator-validate":
new AuthenticatorValidateStage(this, this.challenge).render();
return;
default:
this.container.innerText =
"Unsupported stage: " + this.challenge?.component;
return;
}
}
}
export interface FlowInfoChallenge {
flowInfo?: ContextualFlowInfo;
responseErrors?: {
@ -271,13 +198,10 @@ export interface AuthAssertion {
}
class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge> {
deviceChallenge?: DeviceChallenge;
declare deviceChallenge?: DeviceChallenge;
b64enc(buf: Uint8Array): string {
return fromByteArray(buf)
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=/g, "");
return fromByteArray(buf).replace(/\+/g, "-").replace(/\//g, "_").replace(/[=]/g, "");
}
b64RawEnc(buf: Uint8Array): string {
@ -285,9 +209,8 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
}
u8arr(input: string): Uint8Array {
return Uint8Array.from(
atob(input.replace(/_/g, "/").replace(/-/g, "+")),
(c) => c.charCodeAt(0),
return Uint8Array.from(atob(input.replace(/_/g, "/").replace(/-/g, "+")), (c) =>
c.charCodeAt(0),
);
}
@ -295,13 +218,8 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
if ("credentials" in navigator) {
return true;
}
if (
window.location.protocol === "http:" &&
window.location.hostname !== "localhost"
) {
console.warn(
"WebAuthn requires this page to be accessed via HTTPS.",
);
if (window.location.protocol === "http:" && window.location.hostname !== "localhost") {
console.warn("WebAuthn requires this page to be accessed via HTTPS.");
return false;
}
console.warn("WebAuthn not supported by browser.");
@ -322,9 +240,7 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
// string, then a byte array, re-encode it and wrap that in an array.
const stringId = decodeURIComponent(window.atob(userId));
user.id = this.u8arr(this.b64enc(this.u8arr(stringId)));
const challenge = this.u8arr(
credentialCreateOptions.challenge.toString(),
);
const challenge = this.u8arr(credentialCreateOptions.challenge.toString());
return Object.assign({}, credentialCreateOptions, {
challenge,
@ -337,28 +253,19 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
* for posting to the server.
* @param {PublicKeyCredential} newAssertion
*/
transformNewAssertionForServer(
newAssertion: PublicKeyCredential,
): Assertion {
transformNewAssertionForServer(newAssertion: PublicKeyCredential): Assertion {
const attObj = new Uint8Array(
(
newAssertion.response as AuthenticatorAttestationResponse
).attestationObject,
);
const clientDataJSON = new Uint8Array(
newAssertion.response.clientDataJSON,
(newAssertion.response as AuthenticatorAttestationResponse).attestationObject,
);
const clientDataJSON = new Uint8Array(newAssertion.response.clientDataJSON);
const rawId = new Uint8Array(newAssertion.rawId);
const registrationClientExtensions =
newAssertion.getClientExtensionResults();
const registrationClientExtensions = newAssertion.getClientExtensionResults();
return {
id: newAssertion.id,
rawId: this.b64enc(rawId),
type: newAssertion.type,
registrationClientExtensions: JSON.stringify(
registrationClientExtensions,
),
registrationClientExtensions: JSON.stringify(registrationClientExtensions),
response: {
clientDataJSON: this.b64enc(clientDataJSON),
attestationObject: this.b64enc(attObj),
@ -369,16 +276,14 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
transformCredentialRequestOptions(
credentialRequestOptions: PublicKeyCredentialRequestOptions,
): PublicKeyCredentialRequestOptions {
const challenge = this.u8arr(
credentialRequestOptions.challenge.toString(),
);
const challenge = this.u8arr(credentialRequestOptions.challenge.toString());
const allowCredentials = (
credentialRequestOptions.allowCredentials || []
).map((credentialDescriptor) => {
const allowCredentials = (credentialRequestOptions.allowCredentials || []).map(
(credentialDescriptor) => {
const id = this.u8arr(credentialDescriptor.id.toString());
return Object.assign({}, credentialDescriptor, { id });
});
},
);
return Object.assign({}, credentialRequestOptions, {
challenge,
@ -390,25 +295,19 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
* Encodes the binary data in the assertion into strings for posting to the server.
* @param {PublicKeyCredential} newAssertion
*/
transformAssertionForServer(
newAssertion: PublicKeyCredential,
): AuthAssertion {
const response =
newAssertion.response as AuthenticatorAssertionResponse;
transformAssertionForServer(newAssertion: PublicKeyCredential): AuthAssertion {
const response = newAssertion.response as AuthenticatorAssertionResponse;
const authData = new Uint8Array(response.authenticatorData);
const clientDataJSON = new Uint8Array(response.clientDataJSON);
const rawId = new Uint8Array(newAssertion.rawId);
const sig = new Uint8Array(response.signature);
const assertionClientExtensions =
newAssertion.getClientExtensionResults();
const assertionClientExtensions = newAssertion.getClientExtensionResults();
return {
id: newAssertion.id,
rawId: this.b64enc(rawId),
type: newAssertion.type,
assertionClientExtensions: JSON.stringify(
assertionClientExtensions,
),
assertionClientExtensions: JSON.stringify(assertionClientExtensions),
response: {
clientDataJSON: this.b64RawEnc(clientDataJSON),
@ -421,8 +320,10 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
render() {
if (!this.deviceChallenge) {
return this.renderChallengePicker();
this.renderChallengePicker();
return;
}
switch (this.deviceChallenge.deviceClass) {
case "static":
case "totp":
@ -437,10 +338,8 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
}
renderChallengePicker() {
const challenges = this.challenge.deviceChallenges.filter(
(challenge) =>
challenge.deviceClass === "webauthn" &&
!this.checkWebAuthnSupport()
const challenges = this.challenge.deviceChallenges.filter((challenge) =>
challenge.deviceClass === "webauthn" && !this.checkWebAuthnSupport()
? undefined
: challenge,
);
@ -480,12 +379,13 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
.join("")}
</form>`);
this.challenge.deviceChallenges.forEach((challenge) => {
$(
`#picker-form button#${challenge.deviceClass}-${challenge.deviceUid}`,
).on("click", () => {
$(`#picker-form button#${challenge.deviceClass}-${challenge.deviceUid}`).on(
"click",
() => {
this.deviceChallenge = challenge;
this.render();
});
},
);
});
}
@ -523,8 +423,7 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
navigator.credentials
.get({
publicKey: this.transformCredentialRequestOptions(
this.deviceChallenge
?.challenge as PublicKeyCredentialRequestOptions,
this.deviceChallenge?.challenge as PublicKeyCredentialRequestOptions,
),
})
.then((assertion) => {
@ -534,8 +433,7 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
try {
// we now have an authentication assertion! encode the byte arrays contained
// in the assertion data as strings for posting to the server
const transformedAssertionForServer =
this.transformAssertionForServer(
const transformedAssertionForServer = this.transformAssertionForServer(
assertion as PublicKeyCredential,
);
@ -544,9 +442,7 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
webauthn: transformedAssertionForServer,
});
} catch (err) {
throw new Error(
`Error when validating assertion on server: ${err}`,
);
throw new Error(`Error when validating assertion on server: ${err}`);
}
})
.catch((error) => {
@ -557,7 +453,87 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
}
}
const sfe = new SimpleFlowExecutor(
$("#flow-sfe-container")[0] as HTMLDivElement,
);
class SimpleFlowExecutor {
challenge?: ChallengeTypes;
flowSlug: string;
container: HTMLDivElement;
constructor(container: HTMLDivElement) {
this.flowSlug = window.location.pathname.split("/")[3] || "";
this.container = container;
}
get apiURL() {
return `${ak().api.base}api/v3/flows/executor/${this.flowSlug}/?query=${encodeURIComponent(window.location.search.substring(1))}`;
}
start() {
$.ajax({
type: "GET",
url: this.apiURL,
success: (data) => {
this.challenge = ChallengeTypesFromJSON(data);
this.renderChallenge();
},
});
}
submit(payload: { [key: string]: unknown } | FormData) {
$("button[type=submit]").addClass("disabled")
.html(`<span class="spinner-border spinner-border-sm" aria-hidden="true"></span>
<span role="status">Loading...</span>`);
let finalData: { [key: string]: unknown } = {};
if (payload instanceof FormData) {
finalData = {};
payload.forEach((value, key) => {
finalData[key] = value;
});
} else {
finalData = payload;
}
$.ajax({
type: "POST",
url: this.apiURL,
data: JSON.stringify(finalData),
success: (data) => {
this.challenge = ChallengeTypesFromJSON(data);
this.renderChallenge();
},
contentType: "application/json",
dataType: "json",
});
}
renderChallenge() {
switch (this.challenge?.component) {
case "ak-stage-identification":
new IdentificationStage(this, this.challenge).render();
return;
case "ak-stage-password":
new PasswordStage(this, this.challenge).render();
return;
case "xak-flow-redirect":
new RedirectStage(this, this.challenge).render();
return;
case "ak-stage-autosubmit":
new AutosubmitStage(this, this.challenge).render();
return;
case "ak-stage-authenticator-validate":
new AuthenticatorValidateStage(this, this.challenge).render();
return;
default:
this.container.innerText = `Unsupported stage: ${this.challenge?.component}`;
return;
}
}
}
const [flowContainer] = $<HTMLDivElement>("#flow-sfe-container");
if (!flowContainer) {
throw new Error("No flow container element found");
}
const sfe = new SimpleFlowExecutor(flowContainer);
sfe.start();

View File

@ -1,8 +1,8 @@
{
"extends": "@goauthentik/tsconfig",
"compilerOptions": {
"types": ["jquery"],
"emitDeclarationOnly": true,
"baseUrl": ".",
"esModuleInterop": true,
"lib": ["DOM", "ES2015", "ES2017"]
}

22
web/paths.js Normal file
View File

@ -0,0 +1,22 @@
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
/**
* @typedef {'@goauthentik/web'} WebPackageIdentifier
*/
/**
* The root of the web package.
*/
export const PackageRoot = /** @type {WebPackageIdentifier} */ (resolve(__dirname));
/**
* Path to the web package's distribution directory.
*
* This is where the built files are located after running the build process.
*/
export const DistDirectory = /** @type {`${WebPackageIdentifier}/dist`} */ (
resolve(__dirname, "dist")
);

View File

@ -1,72 +1,103 @@
import { spawnSync } from "child_process";
import fs from "fs";
import path from "path";
import process from "process";
/**
* @file Lit Localize build script.
*
* @import { Config } from "@lit/localize-tools/lib/types/config.js"
*/
import * as fs from "node:fs/promises";
import * as path from "node:path";
import process from "node:process";
import { $ } from "zx";
const localizeRules = await import("../lit-localize.json", {
with: {
type: "json",
},
})
.then((module) => {
return /** @type {Config} */ (module.default);
})
.catch((error) => {
console.error("Failed to load lit-localize.json", error);
process.exit(1);
});
/**
* 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.
* Attempt to stat a file, returning null if it doesn't exist.
*/
function tryStat(filePath) {
return fs.stat(filePath).catch(() => null);
}
const localizeRules = JSON.parse(fs.readFileSync("./lit-localize.json", "utf-8"));
/**
* Check if a generated file is up-to-date with its XLIFF source.
*
* @param {string} languageCode The locale to check.
*/
async function generatedFileIsUpToDateWithXliffSource(languageCode) {
const xlfFilePath = path.join("./xliff", `${languageCode}.xlf`);
const xlfStat = await tryStat(xlfFilePath);
function generatedFileIsUpToDateWithXliffSource(loc) {
const xliff = path.join("./xliff", `${loc}.xlf`);
const gened = path.join("./src/locales", `${loc}.ts`);
if (!xlfStat) {
console.error(`lit-localize expected '${languageCode}.xlf', but XLF file is not present`);
// Returns false if: the expected XLF file doesn't exist, The expected
// generated file doesn't exist, or the XLF file is newer (has a higher date)
// than the generated file. The missing XLF file is important enough it
// generates a unique error message and halts the build.
try {
var xlfStat = fs.statSync(xliff);
} catch (_error) {
console.error(`lit-localize expected '${loc}.xlf', but XLF file is not present`);
process.exit(1);
}
// If the generated file doesn't exist, of course it's not up to date.
try {
var genedStat = fs.statSync(gened);
} catch (_error) {
return false;
const generatedTSFilePath = path.join("./src/locales", `${languageCode}.ts`);
const generatedTSFilePathStat = await tryStat(generatedTSFilePath);
// Does the generated file exist?
if (!generatedTSFilePathStat) {
return {
languageCode,
exists: false,
expired: null,
};
}
// if the generated file is the same age or newer (date is greater) than the xliff file, it's
// presumed to have been generated by that file and is up-to-date.
return genedStat.mtimeMs >= xlfStat.mtimeMs;
return {
languageCode,
exists: true,
// Is the generated file older than the XLIFF file?
expired: generatedTSFilePathStat.mtimeMs < xlfStat.mtimeMs,
};
}
// For all the expected files, find out if any aren't up-to-date.
const upToDate = localizeRules.targetLocales.reduce(
(acc, loc) => acc && generatedFileIsUpToDateWithXliffSource(loc),
true,
const results = await Promise.all(
localizeRules.targetLocales.map(generatedFileIsUpToDateWithXliffSource),
);
if (!upToDate) {
const status = spawnSync("npm", ["run", "build-locales:build"], { encoding: "utf8" });
const pendingBuild = results.some((result) => !result.exists || result.expired);
if (!pendingBuild) {
console.log("Local is up-to-date!");
process.exit(0);
}
const status = await $({ stdio: ["ignore", "pipe", "pipe"] })`npx lit-localize build`;
/**
* @type {Map<string, number>}
*/
const counts = new Map();
// Count all the missing message warnings
const counts = status.stderr.split("\n").reduce((acc, line) => {
for (const line of status.stderr.split("\n")) {
const match = /^([\w-]+) message/.exec(line);
if (!match) {
return acc;
}
acc.set(match[1], (acc.get(match[1]) || 0) + 1);
return acc;
}, new Map());
if (!match) continue;
const locales = Array.from(counts.keys());
locales.sort();
const report = locales
.map((locale) => `Locale '${locale}' has ${counts.get(locale)} missing translations`)
.join("\n");
console.log(`Translation tables rebuilt.\n${report}\n`);
const count = counts.get(match[1]) || 0;
counts.set(match[1], count + 1);
}
console.log("Locale ./src is up-to-date");
const locales = Array.from(counts.keys()).sort();
for (const locale of locales) {
console.log(`Locale '${locale}' has ${counts.get(locale)} missing translations`);
}
await $`npx prettier --write src/locale-codes.ts`;
console.log("\nTranslation tables rebuilt.\n");

79
web/scripts/build-sfe.mjs Normal file
View File

@ -0,0 +1,79 @@
/**
* @file Build script for the simplified flow executor (SFE).
*/
import { DistDirectory, PackageRoot } from "@goauthentik/web/paths";
import esbuild from "esbuild";
import copy from "esbuild-plugin-copy";
import { es5Plugin } from "esbuild-plugin-es5";
import { createRequire } from "node:module";
import * as path from "node:path";
const require = createRequire(import.meta.url);
async function buildSFE() {
const sourceDirectory = path.join(PackageRoot, "packages", "sfe");
const outDirectory = path.join(DistDirectory, "sfe");
const bootstrapCSSPath = require.resolve(
path.join("bootstrap", "dist", "css", "bootstrap.min.css"),
);
/**
* @type {esbuild.BuildOptions}
*/
const config = {
tsconfig: path.join(sourceDirectory, "tsconfig.json"),
entryPoints: [path.join(sourceDirectory, "index.ts")],
minify: false,
bundle: true,
sourcemap: true,
legalComments: "external",
platform: "browser",
format: "iife",
alias: {
"@swc/helpers": path.dirname(require.resolve("@swc/helpers/package.json")),
},
banner: {
js: [
// ---
"// Simplified Flow Executor (SFE)",
"// @ts-nocheck",
"",
].join("\n"),
},
plugins: [
copy({
assets: [
{
from: bootstrapCSSPath,
to: outDirectory,
},
],
}),
es5Plugin({
swc: {
jsc: {
loose: false,
externalHelpers: false,
keepClassNames: false,
},
minify: false,
},
}),
],
target: ["es5"],
outdir: outDirectory,
};
esbuild.build(config);
}
buildSFE()
.then(() => {
console.log("Build complete");
})
.catch((error) => {
console.error("Build failed", error);
process.exit(1);
});

View File

@ -1,14 +1,14 @@
import { execFileSync } from "child_process";
import { deepmerge } from "deepmerge-ts";
import esbuild from "esbuild";
import { polyfillNode } from "esbuild-plugin-polyfill-node";
import findFreePorts from "find-free-ports";
import { copyFileSync, mkdirSync, readFileSync, statSync } from "fs";
import { globSync } from "glob";
import { execFileSync } from "node:child_process";
import { cwd } from "node:process";
import process from "node:process";
import { fileURLToPath } from "node:url";
import * as path from "path";
import { cwd } from "process";
import process from "process";
import { fileURLToPath } from "url";
import { mdxPlugin } from "./esbuild/build-mdx-plugin.mjs";
import { buildObserverPlugin } from "./esbuild/build-observer-plugin.mjs";
@ -117,6 +117,7 @@ const BASE_ESBUILD_OPTIONS = {
write: true,
sourcemap: true,
minify: NODE_ENV === "production",
legalComments: "external",
splitting: true,
treeShaking: true,
external: ["*.woff", "*.woff2"],

View File

@ -1,5 +1,5 @@
import { execSync } from "child_process";
import path from "path";
import { execSync } from "node:child_process";
import * as path from "node:path";
const projectRoot = execSync("git rev-parse --show-toplevel", { encoding: "utf8" }).replace(
"\n",

View File

@ -40,22 +40,13 @@ const name = "mdx-plugin";
* @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.
* @param {LoadData} data Data.
* @returns {Promise<OnLoadResult>} Result.
*/
async function onload(data) {
const content = String(
@ -77,5 +68,9 @@ export function mdxPlugin({ root }) {
loader: "file",
};
}
build.onLoad({ filter: /\.mdx?$/ }, onload);
}
return { name, setup };
}

View File

@ -1,5 +1,5 @@
import * as http from "http";
import path from "path";
import * as path from "node:path";
/**
* Serializes a custom event to a text stream.
@ -13,7 +13,7 @@ export function serializeCustomEventToStream(event) {
const eventContent = [`event: ${event.type}`, `data: ${JSON.stringify(data)}`];
return eventContent.join("\n") + "\n\n";
return `${eventContent.join("\n")}\n\n`;
}
/**
@ -70,6 +70,13 @@ export function buildObserverPlugin({ serverURL, logPrefix, relativeRoot }) {
dispatcher.addEventListener("esbuild:error", listener);
dispatcher.addEventListener("esbuild:end", listener);
const keepAliveInterval = setInterval(() => {
console.timeStamp("🏓 Keep-alive");
res.write("event: keep-alive\n\n");
res.write(serializeCustomEventToStream(new CustomEvent("esbuild:keep-alive")));
}, 15_000);
req.on("close", () => {
console.log("🔌 Client disconnected");
@ -79,13 +86,6 @@ export function buildObserverPlugin({ serverURL, logPrefix, relativeRoot }) {
dispatcher.removeEventListener("esbuild:error", listener);
dispatcher.removeEventListener("esbuild:end", listener);
});
const keepAliveInterval = setInterval(() => {
console.timeStamp("🏓 Keep-alive");
res.write("event: keep-alive\n\n");
res.write(serializeCustomEventToStream(new CustomEvent("esbuild:keep-alive")));
}, 15_000);
});
return {

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,87 +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/",
"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,22 +1,47 @@
import { readFileSync } from "fs";
import path from "path";
/**
* @import { Config } from "@lit/localize-tools/lib/types/config.js"
* @import { TransformOutputConfig } from "@lit/localize-tools/lib/types/modes.js"
* @import { Locale } from "@lit/localize-tools/lib/types/locale.js"
* @import { ProgramMessage } from "@lit/localize-tools/lib/messages.js"
* @import { Message } from "@lit/localize-tools/lib/messages.js"
*
* @typedef {Config & { output: TransformOutputConfig; }} TransformerConfig
*/
import * as path from "node:path";
import { fileURLToPath } from "node:url";
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"));
const baseConfig = await import("../lit-localize.json", {
with: {
type: "json",
},
})
.then((module) => {
return /** @type {Config} */ (module.default);
})
.catch((error) => {
console.error("Failed to load lit-localize.json", error);
process.exit(1);
});
// 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.
/**
* @type {TransformerConfig}
*/
const config = {
...baseConfig,
baseDir: path.join(__dirname, ".."),
@ -25,18 +50,26 @@ const config = {
...baseConfig.output,
mode: "transform",
},
resolve: (path) => path,
resolve: (filePath) => filePath,
};
const pseudoMessagify = (message) => ({
name: message.name,
contents: message.contents.map((content) =>
typeof content === "string" ? pseudolocale(content, { prepend: "", append: "" }) : content,
/**
* @param {ProgramMessage} message
* @returns {Message}
*/
function pseudoMessagify({ name, contents }) {
return {
name,
contents: contents.map((content) =>
typeof content === "string"
? pseudolocale(content, { prepend: "", append: "" })
: content,
),
});
};
}
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

@ -19,7 +19,7 @@ import { AdminApi, CapabilitiesEnum, LicenseSummaryStatusEnum } from "@goauthent
@customElement("ak-about-modal")
export class AboutModal extends WithLicenseSummary(WithBrandConfig(ModalButton)) {
static get styles() {
return super.styles.concat(
return ModalButton.styles.concat(
PFAbout,
css`
.pf-c-about-modal-box__hero {
@ -59,7 +59,7 @@ export class AboutModal extends WithLicenseSummary(WithBrandConfig(ModalButton))
renderModal() {
let product = globalAK().brand.brandingTitle || DefaultBrand.brandingTitle;
if (this.licenseSummary.status != LicenseSummaryStatusEnum.Unlicensed) {
if (this.licenseSummary.status !== LicenseSummaryStatusEnum.Unlicensed) {
product += ` ${msg("Enterprise")}`;
}
return html`<div

View File

@ -125,6 +125,7 @@ export class AdminInterface extends AuthenticatedInterface {
if (process.env.NODE_ENV === "development" && process.env.WATCHER_URL) {
const { ESBuildObserver } = await import("@goauthentik/common/client");
// eslint-disable-next-line no-new
new ESBuildObserver(process.env.WATCHER_URL);
}
}

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

@ -28,17 +28,19 @@ export class WorkersStatusCard extends AdminStatusCard<Worker[]> {
icon: "fa fa-times-circle pf-m-danger",
message: html`${msg("No workers connected. Background tasks will not run.")}`,
});
} else if (value.filter((w) => !w.versionMatching).length > 0) {
}
if (value.filter((w) => !w.versionMatching).length > 0) {
return Promise.resolve<AdminStatus>({
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",
});
}
}
renderValue() {
return html`${this.value?.length}`;

View File

@ -127,7 +127,7 @@ export class SyncStatusChart extends AKChart<SummarizedSyncStatus[]> {
msg("LDAP Source"),
),
];
this.centerText = statuses.reduce((total, el) => (total += el.total), 0).toString();
this.centerText = statuses.reduce((total, el) => total + el.total, 0).toString();
return statuses;
}

View File

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

View File

@ -79,7 +79,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,
@ -117,7 +117,7 @@ export class ApplicationForm extends WithCapabilitiesConfig(ModelForm<Applicatio
if (!(ev instanceof InputEvent) || !ev.target) {
return;
}
this.clearIcon = !!(ev.target as HTMLInputElement).checked;
this.clearIcon = Boolean((ev.target as HTMLInputElement).checked);
}
renderForm(): TemplateResult {

View File

@ -71,7 +71,7 @@ export class ApplicationListPage extends WithBrandConfig(TablePage<Application>)
}
static get styles(): CSSResult[] {
return super.styles.concat(PFCard, applicationListStyle);
return TablePage.styles.concat(PFCard, applicationListStyle);
}
columns(): TableColumn[] {

View File

@ -6,7 +6,7 @@ import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
import "@goauthentik/elements/forms/Radio";
import "@goauthentik/elements/forms/SearchSelect";
import YAML from "yaml";
import * as YAML from "yaml";
import { msg } from "@lit/localize";
import { CSSResult } from "lit";
@ -31,9 +31,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,12 +48,11 @@ export class ApplicationEntitlementForm extends ModelForm<ApplicationEntitlement
pbmUuid: this.instance.pbmUuid || "",
applicationEntitlementRequest: data,
});
} else {
}
return new CoreApi(DEFAULT_CONFIG).coreApplicationEntitlementsCreate({
applicationEntitlementRequest: data,
});
}
}
renderForm(): TemplateResult {
return html` <ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">

View File

@ -52,7 +52,6 @@ function renderRadiusOverview(rawProvider: OneOfProvider) {
}
function renderRACOverview(rawProvider: OneOfProvider) {
// @ts-expect-error TS6133
const _provider = rawProvider as RACProvider;
}

View File

@ -37,7 +37,7 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
errors = new Map<string, string>();
@query("form#applicationform")
form!: HTMLFormElement;
declare form: HTMLFormElement;
constructor() {
super();
@ -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

@ -45,7 +45,7 @@ export class ApplicationWizardEditBindingStep extends ApplicationWizardStep {
hide = true;
@query("form#bindingform")
form!: HTMLFormElement;
declare form: HTMLFormElement;
@query(".policy-search-select")
searchSelect!: SearchSelectBase<Policy> | SearchSelectBase<Group> | SearchSelectBase<User>;

View File

@ -22,7 +22,7 @@ export class ApplicationWizardProviderSamlForm extends ApplicationWizardProvider
const setHasSigningKp = (ev: InputEvent) => {
const target = ev.target as AkCryptoCertificateSearch;
if (!target) return;
this.hasSigningKp = !!target.selectedKeypair;
this.hasSigningKp = Boolean(target.selectedKeypair);
};
return html` <ak-wizard-title>${this.label}</ak-wizard-title>

View File

@ -8,7 +8,7 @@ import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
import "@goauthentik/elements/forms/SearchSelect";
import YAML from "yaml";
import * as YAML from "yaml";
import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, html } from "lit";
@ -59,12 +59,11 @@ export class BlueprintForm extends ModelForm<BlueprintInstance, string> {
instanceUuid: this.instance.pk,
blueprintInstanceRequest: data,
});
} else {
}
return new ManagedApi(DEFAULT_CONFIG).managedBlueprintsCreate({
blueprintInstanceRequest: data,
});
}
}
renderForm(): TemplateResult {
return html` <ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">

View File

@ -9,7 +9,7 @@ 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 * as YAML from "yaml";
import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit";
@ -43,12 +43,11 @@ export class BrandForm extends ModelForm<Brand, string> {
brandUuid: this.instance.brandUuid,
brandRequest: data,
});
} else {
}
return new CoreApi(DEFAULT_CONFIG).coreBrandsCreate({
brandRequest: data,
});
}
}
renderForm(): TemplateResult {
return html` <ak-form-element-horizontal label=${msg("Domain")} required name="domain">

View File

@ -107,7 +107,7 @@ export class AkCryptoCertificateSearch extends CustomListenerElement(AKElement)
selected(item: CertificateKeyPair, items: CertificateKeyPair[]) {
return (
(this.singleton && !this.certificate && items.length === 1) ||
(!!this.certificate && this.certificate === item.pk)
(Boolean(this.certificate) && this.certificate === item.pk)
);
}

View File

@ -29,7 +29,7 @@ const metadata: Meta<AkCryptoCertificateSearch> = {
argTypes: {
// Typescript is unaware that arguments for components are treated as properties, and
// properties are typically renamed to lower case, even if the variable is not.
// @ts-expect-error
// @ts-expect-error TODO: Explain.
nokey: {
control: "boolean",
description:
@ -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,12 +30,11 @@ 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,
});
}
}
renderForm(): TemplateResult {
return html` <ak-form-element-horizontal label=${msg("Name")} name="name" ?required=${true}>

View File

@ -51,12 +51,11 @@ export class RuleForm extends ModelForm<NotificationRule, string> {
pbmUuid: this.instance.pk || "",
notificationRuleRequest: data,
});
} else {
}
return new EventsApi(DEFAULT_CONFIG).eventsRulesCreate({
notificationRuleRequest: data,
});
}
}
renderForm(): TemplateResult {
return html` <ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">

View File

@ -47,12 +47,11 @@ export class TransportForm extends ModelForm<NotificationTransport, string> {
uuid: this.instance.pk || "",
notificationTransportRequest: data,
});
} else {
}
return new EventsApi(DEFAULT_CONFIG).eventsTransportsCreate({
notificationTransportRequest: data,
});
}
}
onModeChange(mode: string | undefined): void {
if (

View File

@ -58,7 +58,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,

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

@ -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,7 +49,7 @@ export class StageBindingForm extends ModelForm<FlowStageBinding, string> {
fsbUuid: this.instance.pk,
patchedFlowStageBindingRequest: data,
});
} else {
}
if (this.targetPk) {
data.target = this.targetPk;
}
@ -58,7 +57,6 @@ export class StageBindingForm extends ModelForm<FlowStageBinding, string> {
flowStageBindingRequest: data,
});
}
}
async getOrder(): Promise<number> {
if (this.instance?.pk) {

View File

@ -10,7 +10,7 @@ import "@goauthentik/elements/chips/ChipGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
import "@goauthentik/elements/forms/SearchSelect";
import YAML from "yaml";
import * as YAML from "yaml";
import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, css, html } from "lit";
@ -55,13 +55,12 @@ export class GroupForm extends ModelForm<Group, string> {
groupUuid: this.instance.pk,
patchedGroupRequest: data,
});
} else {
}
data.users = [];
return new CoreApi(DEFAULT_CONFIG).coreGroupsCreate({
groupRequest: data,
});
}
}
renderForm(): TemplateResult {
return html` <ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">

View File

@ -125,7 +125,8 @@ export class RelatedGroupList extends Table<Group> {
buttonLabel=${msg("Remove")}
.objects=${this.selectedElements}
.delete=${(item: Group) => {
if (!this.targetUser) return;
if (!this.targetUser) return null;
return new CoreApi(DEFAULT_CONFIG).coreGroupsRemoveUserCreate({
groupUuid: item.pk,
userAccountRequest: {

View File

@ -46,8 +46,12 @@ import {
User,
} from "@goauthentik/api";
interface AddUsersToGroupFormData {
users: number[];
}
@customElement("ak-user-related-add")
export class RelatedUserAdd extends Form<{ users: number[] }> {
export class RelatedUserAdd extends Form<AddUsersToGroupFormData> {
@property({ attribute: false })
group?: Group;
@ -58,7 +62,7 @@ export class RelatedUserAdd extends Form<{ users: number[] }> {
return msg("Successfully added user(s).");
}
async send(data: { users: number[] }): Promise<{ users: number[] }> {
async send(data: AddUsersToGroupFormData): Promise<AddUsersToGroupFormData> {
await Promise.all(
data.users.map((user) => {
return new CoreApi(DEFAULT_CONFIG).coreGroupsAddUserCreate({
@ -69,6 +73,7 @@ export class RelatedUserAdd extends Form<{ users: number[] }> {
});
}),
);
return data;
}
@ -133,7 +138,7 @@ export class RelatedUserList extends WithBrandConfig(WithCapabilitiesConfig(Tabl
me?: SessionUser;
static get styles(): CSSResult[] {
return super.styles.concat(PFDescriptionList, PFAlert, PFBanner);
return Table.styles.concat(PFDescriptionList, PFAlert, PFBanner);
}
async apiEndpoint(): Promise<PaginatedResponse<User>> {

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

@ -10,7 +10,7 @@ import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
import "@goauthentik/elements/forms/SearchSelect";
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
import YAML from "yaml";
import * as YAML from "yaml";
import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit";
@ -129,12 +129,11 @@ export class OutpostForm extends ModelForm<Outpost, string> {
uuid: this.instance.pk || "",
outpostRequest: data,
});
} else {
}
return new OutpostsApi(DEFAULT_CONFIG).outpostsInstancesCreate({
outpostRequest: data,
});
}
}
renderForm(): TemplateResult {
const typeOptions = [

View File

@ -32,12 +32,11 @@ export class ServiceConnectionDockerForm extends ModelForm<DockerServiceConnecti
uuid: this.instance.pk || "",
dockerServiceConnectionRequest: data,
});
} else {
}
return new OutpostsApi(DEFAULT_CONFIG).outpostsServiceConnectionsDockerCreate({
dockerServiceConnectionRequest: data,
});
}
}
renderForm(): TemplateResult {
return html` <ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">

View File

@ -4,7 +4,7 @@ import "@goauthentik/elements/CodeMirror";
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
import YAML from "yaml";
import * as YAML from "yaml";
import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit";
@ -36,12 +36,11 @@ export class ServiceConnectionKubernetesForm extends ModelForm<
uuid: this.instance.pk || "",
kubernetesServiceConnectionRequest: data,
});
} else {
}
return new OutpostsApi(DEFAULT_CONFIG).outpostsServiceConnectionsKubernetesCreate({
kubernetesServiceConnectionRequest: data,
});
}
}
renderForm(): TemplateResult {
return html` <ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">

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

@ -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,12 +110,11 @@ export class PolicyBindingForm extends ModelForm<PolicyBinding, string> {
policyBindingUuid: this.instance.pk,
policyBindingRequest: data,
});
} else {
}
return new PoliciesApi(DEFAULT_CONFIG).policiesBindingsCreate({
policyBindingRequest: data,
});
}
}
async getOrder(): Promise<number> {
if (this.instance?.pk) {

View File

@ -7,7 +7,7 @@ import "@goauthentik/elements/events/LogViewer";
import { Form } from "@goauthentik/elements/forms/Form";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/SearchSelect";
import YAML from "yaml";
import * as YAML from "yaml";
import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, html } from "lit";

View File

@ -25,12 +25,12 @@ export class DummyPolicyForm extends BasePolicyForm<DummyPolicy> {
policyUuid: this.instance.pk || "",
dummyPolicyRequest: data,
});
} else {
}
return new PoliciesApi(DEFAULT_CONFIG).policiesDummyCreate({
dummyPolicyRequest: data,
});
}
}
renderForm(): TemplateResult {
return html` <span>

View File

@ -37,12 +37,11 @@ export class EventMatcherPolicyForm extends BasePolicyForm<EventMatcherPolicy> {
policyUuid: this.instance.pk || "",
eventMatcherPolicyRequest: data,
});
} else {
}
return new PoliciesApi(DEFAULT_CONFIG).policiesEventMatcherCreate({
eventMatcherPolicyRequest: data,
});
}
}
renderForm(): TemplateResult {
return html` <span>

View File

@ -25,12 +25,11 @@ export class PasswordExpiryPolicyForm extends BasePolicyForm<PasswordExpiryPolic
policyUuid: this.instance.pk || "",
passwordExpiryPolicyRequest: data,
});
} else {
}
return new PoliciesApi(DEFAULT_CONFIG).policiesPasswordExpiryCreate({
passwordExpiryPolicyRequest: data,
});
}
}
renderForm(): TemplateResult {
return html` <span>

View File

@ -28,12 +28,11 @@ export class ExpressionPolicyForm extends BasePolicyForm<ExpressionPolicy> {
policyUuid: this.instance.pk || "",
expressionPolicyRequest: data,
});
} else {
}
return new PoliciesApi(DEFAULT_CONFIG).policiesExpressionCreate({
expressionPolicyRequest: data,
});
}
}
renderForm(): TemplateResult {
return html` <span>

View File

@ -39,12 +39,11 @@ export class GeoIPPolicyForm extends BasePolicyForm<GeoIPPolicy> {
policyUuid: this.instance.pk || "",
geoIPPolicyRequest: data,
});
} else {
}
return new PoliciesApi(DEFAULT_CONFIG).policiesGeoipCreate({
geoIPPolicyRequest: data,
});
}
}
renderForm(): TemplateResult {
return html`<span>

View File

@ -38,12 +38,11 @@ export class PasswordPolicyForm extends BasePolicyForm<PasswordPolicy> {
policyUuid: this.instance.pk || "",
passwordPolicyRequest: data,
});
} else {
}
return new PoliciesApi(DEFAULT_CONFIG).policiesPasswordCreate({
passwordPolicyRequest: data,
});
}
}
renderStaticRules(): TemplateResult {
return html` <ak-form-group>

View File

@ -25,12 +25,11 @@ export class ReputationPolicyForm extends BasePolicyForm<ReputationPolicy> {
policyUuid: this.instance.pk || "",
reputationPolicyRequest: data,
});
} else {
}
return new PoliciesApi(DEFAULT_CONFIG).policiesReputationCreate({
reputationPolicyRequest: data,
});
}
}
renderForm(): TemplateResult {
return html` <span>

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