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] [*.html]
indent_size = 2 indent_size = 2
[schemas/*.json]
indent_size = 2
[*.{yaml,yml}] [*.{yaml,yml}]
indent_size = 2 indent_size = 2

View File

@ -3,11 +3,14 @@
## Static Files ## Static Files
**/LICENSE **/LICENSE
authentik/stages/**/*
## Build asset directories ## Build asset directories
/build
coverage coverage
dist dist
out out
.docusaurus
website/docs/developer-docs/api/**/*
## Environment ## Environment
*.env *.env
@ -20,9 +23,25 @@ out
## Node ## Node
node_modules node_modules
coverage
## Configs ## Configs
*.log *.log
*.yaml *.yaml
*.yml *.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", "ms-python.vscode-pylance",
"redhat.vscode-yaml", "redhat.vscode-yaml",
"Tobermory.es6-string-html", "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.importModuleSpecifier": "non-relative",
"typescript.preferences.importModuleSpecifierEnding": "index", "typescript.preferences.importModuleSpecifierEnding": "index",
"typescript.tsdk": "./web/node_modules/typescript/lib", "typescript.tsdk": "./node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true, "typescript.enablePromptUseWorkspaceTsdk": true,
"yaml.schemas": { "yaml.schemas": {
"./blueprints/schema.json": "blueprints/**/*.yaml" "./blueprints/schema.json": "blueprints/**/*.yaml"
@ -30,7 +30,56 @@
} }
], ],
"go.testFlags": ["-count=1"], "go.testFlags": ["-count=1"],
"github-actions.workflows.pinned.workflows": [ "github-actions.workflows.pinned.workflows": [".github/workflows/ci-main.yml"],
".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", "label": "authentik/core: make",
"command": "uv", "command": "uv",
"args": [ "args": ["run", "make", "lint-fix", "lint"],
"run",
"make",
"lint-fix",
"lint"
],
"presentation": { "presentation": {
"panel": "new" "panel": "new"
}, },
@ -18,11 +13,7 @@
{ {
"label": "authentik/core: run", "label": "authentik/core: run",
"command": "uv", "command": "uv",
"args": [ "args": ["run", "ak", "server"],
"run",
"ak",
"server"
],
"group": "build", "group": "build",
"presentation": { "presentation": {
"panel": "dedicated", "panel": "dedicated",
@ -32,17 +23,13 @@
{ {
"label": "authentik/web: make", "label": "authentik/web: make",
"command": "make", "command": "make",
"args": [ "args": ["web"],
"web"
],
"group": "build" "group": "build"
}, },
{ {
"label": "authentik/web: watch", "label": "authentik/web: watch",
"command": "make", "command": "make",
"args": [ "args": ["web-watch"],
"web-watch"
],
"group": "build", "group": "build",
"presentation": { "presentation": {
"panel": "dedicated", "panel": "dedicated",
@ -52,26 +39,19 @@
{ {
"label": "authentik: install", "label": "authentik: install",
"command": "make", "command": "make",
"args": [ "args": ["install", "-j4"],
"install",
"-j4"
],
"group": "build" "group": "build"
}, },
{ {
"label": "authentik/website: make", "label": "authentik/website: make",
"command": "make", "command": "make",
"args": [ "args": ["website"],
"website"
],
"group": "build" "group": "build"
}, },
{ {
"label": "authentik/website: watch", "label": "authentik/website: watch",
"command": "make", "command": "make",
"args": [ "args": ["website-watch"],
"website-watch"
],
"group": "build", "group": "build",
"presentation": { "presentation": {
"panel": "dedicated", "panel": "dedicated",
@ -81,11 +61,7 @@
{ {
"label": "authentik/api: generate", "label": "authentik/api: generate",
"command": "uv", "command": "uv",
"args": [ "args": ["run", "make", "gen"],
"run",
"make",
"gen"
],
"group": "build" "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 \ --rm -v ${PWD}:/local \
--user ${UID}:${GID} \ --user ${UID}:${GID} \
docker.io/openapitools/openapi-generator-cli:v7.11.0 generate \ docker.io/openapitools/openapi-generator-cli:v7.11.0 generate \
-i /local/schema.yml \ --input-spec /local/schema.yml \
-g typescript-fetch \ --generator-name typescript-fetch \
-o /local/${GEN_API_TS} \ --output /local/${GEN_API_TS} \
-c /local/scripts/api-ts-config.yaml \ --config /local/scripts/api-ts-config.yaml \
--additional-properties=npmVersion=${NPM_VERSION} \ --additional-properties=npmVersion=${NPM_VERSION} \
--git-repo-id authentik \ --git-repo-id authentik \
--git-user-id goauthentik --git-user-id goauthentik
mkdir -p web/node_modules/@goauthentik/api npm install
cd ./${GEN_API_TS} && npm i
\cp -rf ./${GEN_API_TS}/* web/node_modules/@goauthentik/api
gen-client-py: gen-clean-py ## Build and install the authentik API for Python gen-client-py: gen-clean-py ## Build and install the authentik API for Python
docker run \ docker run \

View File

@ -323,12 +323,7 @@
"multiValued": false, "multiValued": false,
"description": "Indicates whether or not an attribute is modifiable.", "description": "Indicates whether or not an attribute is modifiable.",
"required": false, "required": false,
"canonicalValues": [ "canonicalValues": ["readOnly", "readWrite", "immutable", "writeOnly"],
"readOnly",
"readWrite",
"immutable",
"writeOnly"
],
"caseExact": false, "caseExact": false,
"mutability": "readOnly", "mutability": "readOnly",
"returned": "default", "returned": "default",
@ -340,12 +335,7 @@
"multiValued": false, "multiValued": false,
"description": "Indicates when an attribute is returned in a response (e.g., to a query).", "description": "Indicates when an attribute is returned in a response (e.g., to a query).",
"required": false, "required": false,
"canonicalValues": [ "canonicalValues": ["always", "never", "default", "request"],
"always",
"never",
"default",
"request"
],
"caseExact": false, "caseExact": false,
"mutability": "readOnly", "mutability": "readOnly",
"returned": "default", "returned": "default",
@ -369,12 +359,7 @@
"multiValued": true, "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'.", "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, "required": false,
"canonicalValues": [ "canonicalValues": ["resource", "external", "uri", "url"],
"resource",
"external",
"uri",
"url"
],
"caseExact": false, "caseExact": false,
"mutability": "readOnly", "mutability": "readOnly",
"returned": "default", "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", "name": "@goauthentik/lifecycle-aws",
"version": "0.0.0", "version": "0.0.0",
"private": true,
"license": "MIT",
"scripts": { "scripts": {
"aws-cfn": "cross-env CI=false cdk synth --version-reporting=false > template.yaml" "aws-cfn": "cross-env CI=false cdk synth --version-reporting=false > template.yaml"
}, },
"engines": {
"node": ">=20"
},
"devDependencies": { "devDependencies": {
"aws-cdk": "^2.1005.0", "aws-cdk": "^2.1005.0",
"cross-env": "^7.0.3" "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", "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 eslint from "@eslint/js";
import { javaScriptConfig } from "@goauthentik/eslint-config/javascript-config";
import { reactConfig } from "@goauthentik/eslint-config/react-config"; import { reactConfig } from "@goauthentik/eslint-config/react-config";
import { typescriptConfig } from "@goauthentik/eslint-config/typescript-config"; import { typescriptConfig } from "@goauthentik/eslint-config/typescript-config";
import litconf from "eslint-plugin-lit"; import * as litconf from "eslint-plugin-lit";
import wcconf from "eslint-plugin-wc"; import * as wcconf from "eslint-plugin-wc";
import tseslint from "typescript-eslint"; import tseslint from "typescript-eslint";
// @ts-check // @ts-check
@ -10,7 +11,6 @@ import tseslint from "typescript-eslint";
/** /**
* @typedef ESLintPackageConfigOptions Options for creating package ESLint configuration. * @typedef ESLintPackageConfigOptions Options for creating package ESLint configuration.
* @property {string[]} [ignorePatterns] Override ignore patterns for ESLint. * @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 = [ export const DefaultIgnorePatterns = [
// --- // ---
"**/*.md", "**/*.md",
"**/.yarn",
"**/out", "**/out",
"**/dist", "**/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. * @returns The ESLint configuration object.
*/ */
export function createESLintPackageConfig({ export function createESLintPackageConfig({ ignorePatterns = DefaultIgnorePatterns } = {}) {
ignorePatterns = DefaultIgnorePatterns,
overrides = {},
} = {}) {
return tseslint.config( return tseslint.config(
{ {
ignores: ignorePatterns, ignores: ignorePatterns,
}, },
eslint.configs.recommended, eslint.configs.recommended,
...tseslint.configs.recommended, javaScriptConfig,
eslint.configs.recommended,
wcconf.configs["flat/recommended"], wcconf.configs["flat/recommended"],
litconf.configs["flat/recommended"], litconf.configs["flat/recommended"],
...reactConfig, ...tseslint.configs.recommended,
...typescriptConfig, ...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", "import": "./react-config.js",
"types": "./out/react-config.d.ts" "types": "./out/react-config.d.ts"
}, },
"./javascript-config": {
"import": "./javascript-config.js",
"types": "./out/javascript-config.d.ts"
},
"./typescript-config": { "./typescript-config": {
"import": "./typescript-config.js", "import": "./typescript-config.js",
"types": "./out/typescript-config.d.ts" "types": "./out/typescript-config.d.ts"
} }
}, },
"types": "./out/index.d.ts",
"prettier": "@goauthentik/prettier-config",
"dependencies": { "dependencies": {
"eslint": "^9.23.0", "eslint": "^9.23.0",
"eslint-plugin-import": "^2.31.0", "eslint-plugin-import": "^2.31.0",
@ -30,13 +32,12 @@
"devDependencies": { "devDependencies": {
"@goauthentik/tsconfig": "1.0.0", "@goauthentik/tsconfig": "1.0.0",
"@types/eslint": "^9.6.1", "@types/eslint": "^9.6.1",
"@types/eslint__js": "^9.14.0",
"typescript": "^5.8.2", "typescript": "^5.8.2",
"typescript-eslint": "^8.27.0" "typescript-eslint": "^8.29.0"
}, },
"peerDependencies": { "peerDependencies": {
"typescript": "^5.8.2", "typescript": "^5.8.2",
"typescript-eslint": "^8.27.0" "typescript-eslint": "^8.29.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"react": "^18.3.1" "react": "^18.3.1"
@ -44,6 +45,8 @@
"engines": { "engines": {
"node": ">=20.11" "node": ">=20.11"
}, },
"types": "./out/index.d.ts",
"prettier": "@goauthentik/prettier-config",
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"
} }

View File

@ -1,38 +1,34 @@
import reactPlugin from "eslint-plugin-react" import reactPlugin from "eslint-plugin-react";
import hooksPlugin from "eslint-plugin-react-hooks" import hooksPlugin from "eslint-plugin-react-hooks";
import tseslint from "typescript-eslint" import tseslint from "typescript-eslint";
/** /**
* ESLint configuration for React authentik projects. * ESLint configuration for React authentik projects.
*/ */
export const reactConfig = tseslint.config( export const reactConfig = tseslint.config({
{ settings: {
settings: { react: {
react: { version: "detect",
version: "detect", },
}, },
},
plugins: { plugins: {
// @ts-ignore Fixup plugin rules. "react": reactPlugin,
react: reactPlugin, "react-hooks": hooksPlugin,
// @ts-ignore Fixup plugin },
"react-hooks": hooksPlugin,
},
rules: { rules: {
"react-hooks/rules-of-hooks": "error", "react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn", "react-hooks/exhaustive-deps": "warn",
"react/jsx-uses-react": 0, "react/jsx-uses-react": 0,
"react/display-name": "off", "react/display-name": "off",
"react/jsx-curly-brace-presence": "error", "react/jsx-curly-brace-presence": "error",
"react/jsx-no-leaked-render": "error", "react/jsx-no-leaked-render": "error",
"react/prop-types": "off", "react/prop-types": "off",
"react/react-in-jsx-scope": "off", "react/react-in-jsx-scope": "off",
}, },
} });
)
export default reactConfig export default reactConfig;

View File

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

View File

@ -7,54 +7,28 @@ import tseslint from "typescript-eslint";
export const typescriptConfig = tseslint.config({ export const typescriptConfig = tseslint.config({
rules: { rules: {
"@typescript-eslint/ban-ts-comment": [ "@typescript-eslint/ban-ts-comment": [
"warn", "error",
{ {
"ts-ignore": "allow-with-description", "ts-expect-error": "allow-with-description",
}, "ts-ignore": true,
], "ts-nocheck": "allow-with-description",
"@typescript-eslint/ban-types": "off", "ts-check": false,
"@typescript-eslint/no-empty-interface": "off", "minimumDescriptionLength": 5,
"@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,
}, },
], ],
"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": [ "@typescript-eslint/no-unused-vars": [
"warn", "warn",
{ {
args: "all",
argsIgnorePattern: "^_", argsIgnorePattern: "^_",
caughtErrors: "all", varsIgnorePattern: "^_",
caughtErrorsIgnorePattern: "^_", 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) 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 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, 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. * authentik Prettier configuration.
* *
* @type {import("prettier").Config} * @type {PrettierConfig & SortPluginConfig & PackageJSONPluginConfig}
* @internal * @internal
*/ */
export const AuthentikPrettierConfig = { export const AuthentikPrettierConfig = {
@ -27,6 +37,12 @@ export const AuthentikPrettierConfig = {
importOrderSortSpecifiers: true, importOrderSortSpecifiers: true,
importOrderParserPlugins: ["typescript", "jsx", "classProperties", "decorators-legacy"], importOrderParserPlugins: ["typescript", "jsx", "classProperties", "decorators-legacy"],
overrides: [ overrides: [
{
files: "schemas/**/*.json",
options: {
tabWidth: 2,
},
},
{ {
files: "tsconfig.json", files: "tsconfig.json",
options: { options: {
@ -41,9 +57,22 @@ export const AuthentikPrettierConfig = {
"name", "name",
"version", "version",
"description", "description",
"license",
"private",
"author",
"authors",
"scripts", "scripts",
"devDependencies", "main",
"type",
"exports",
"imports",
"dependencies", "dependencies",
"devDependencies",
"peerDependencies",
"optionalDependencies",
"wireit",
"resolutions",
"engines",
], ],
}, },
}, },

View File

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

View File

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

View File

@ -1,6 +1,7 @@
{ {
"extends": "@goauthentik/tsconfig", "extends": "@goauthentik/tsconfig",
"compilerOptions": { "compilerOptions": {
"baseUrl": ".",
"checkJs": true, "checkJs": true,
"emitDeclarationOnly": 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": { "type": {
"description": "A label indicating the type of resource, e.g., 'User' or 'Group'.", "description": "A label indicating the type of resource, e.g., 'User' or 'Group'.",
"type": "string", "type": "string",
"enum": [ "enum": ["User", "Group"],
"User",
"Group"
],
"readOnly": true "readOnly": true
} }
}, },
@ -39,7 +36,5 @@
} }
} }
}, },
"required": [ "required": ["displayName"]
"displayName"
]
} }

View File

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

View File

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

View File

@ -99,11 +99,7 @@
"type": { "type": {
"description": "A label indicating the attribute's function, e.g., 'work' or 'home'.", "description": "A label indicating the attribute's function, e.g., 'work' or 'home'.",
"type": "string", "type": "string",
"enum": [ "enum": ["work", "home", "other"]
"work",
"home",
"other"
]
}, },
"primary": { "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.", "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": { "type": {
"description": "A label indicating the attribute's function, e.g., 'work', 'home', 'mobile'.", "description": "A label indicating the attribute's function, e.g., 'work', 'home', 'mobile'.",
"type": "string", "type": "string",
"enum": [ "enum": ["work", "home", "mobile", "fax", "pager", "other"]
"work",
"home",
"mobile",
"fax",
"pager",
"other"
]
}, },
"primary": { "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.", "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": { "type": {
"description": "A label indicating the attribute's function, e.g., 'aim', 'gtalk', 'xmpp'.", "description": "A label indicating the attribute's function, e.g., 'aim', 'gtalk', 'xmpp'.",
"type": "string", "type": "string",
"enum": [ "enum": ["aim", "gtalk", "icq", "xmpp", "msn", "skype", "qq", "yahoo"]
"aim",
"gtalk",
"icq",
"xmpp",
"msn",
"skype",
"qq",
"yahoo"
]
}, },
"primary": { "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.", "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": { "type": {
"description": "A label indicating the attribute's function, i.e., 'photo' or 'thumbnail'.", "description": "A label indicating the attribute's function, i.e., 'photo' or 'thumbnail'.",
"type": "string", "type": "string",
"enum": [ "enum": ["photo", "thumbnail"]
"photo",
"thumbnail"
]
}, },
"primary": { "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.", "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": { "type": {
"description": "A label indicating the attribute's function, e.g., 'work' or 'home'.", "description": "A label indicating the attribute's function, e.g., 'work' or 'home'.",
"type": "string", "type": "string",
"enum": [ "enum": ["work", "home", "other"]
"work",
"home",
"other"
]
} }
} }
} }
@ -277,10 +250,7 @@
"type": { "type": {
"description": "A label indicating the attribute's function, e.g., 'direct' or 'indirect'.", "description": "A label indicating the attribute's function, e.g., 'direct' or 'indirect'.",
"type": "string", "type": "string",
"enum": [ "enum": ["direct", "indirect"],
"direct",
"indirect"
],
"readOnly": true "readOnly": true
} }
}, },
@ -366,7 +336,5 @@
} }
} }
}, },
"required": [ "required": ["userName"]
"userName"
]
} }

View File

@ -19,9 +19,8 @@
"files": [], "files": [],
"references": [ "references": [
// Note that references are in the order we want them to be built. // Note that references are in the order we want them to be built.
{ "path": "./packages/tsconfig" },
{ "path": "./packages/prettier-config" }, { "path": "./packages/prettier-config" },
{ "path": "./packages/eslint-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 type { StorybookConfig } from "@storybook/web-components-vite";
import { cwd } from "process"; import { cwd } from "node:process";
import modify from "rollup-plugin-modify"; import modify from "rollup-plugin-modify";
import postcssLit from "rollup-plugin-postcss-lit"; import postcssLit from "rollup-plugin-postcss-lit";
import { mergeConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths"; import tsconfigPaths from "vite-tsconfig-paths";
export const isProdBuild = process.env.NODE_ENV === "production"; export const isProdBuild = process.env.NODE_ENV === "production";
export const apiBasePath = process.env.AK_API_BASE_PATH || ""; 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 = [ const importInlinePatterns = [
'import AKGlobal from "(\\.\\./)*common/styles/authentik\\.css', 'import AKGlobal from "(\\.\\./)*common/styles/authentik\\.css',
'import AKGlobal from "@goauthentik/common/styles/authentik\\.css', 'import AKGlobal from "@goauthentik/common/styles/authentik\\.css',
@ -53,8 +56,14 @@ const config: StorybookConfig = {
autodocs: "tag", autodocs: "tag",
}, },
async viteFinal(config) { async viteFinal(config) {
return { return mergeConfig(config, {
...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: [ plugins: [
modify({ modify({
find: importInlineRegexp, find: importInlineRegexp,
@ -62,19 +71,10 @@ const config: StorybookConfig = {
return `${match}?inline`; 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(), postcssLit(),
tsconfigPaths(), 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", "name": "@goauthentik/web",
"version": "0.0.0", "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": { "scripts": {
"build": "wireit", "build": "wireit",
"build-locales": "wireit", "build-locales": "wireit",
"build-locales:build": "wireit",
"build-proxy": "wireit", "build-proxy": "wireit",
"build:sfe": "wireit", "build:sfe": "wireit",
"esbuild:watch": "node scripts/build-web.mjs --watch", "esbuild:watch": "node scripts/build-web.mjs --watch",
@ -160,7 +31,138 @@
"tsc": "wireit", "tsc": "wireit",
"watch": "run-s build-locales esbuild:watch" "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", "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": { "wireit": {
"build": { "build": {
"#comment": [ "#comment": [
@ -217,12 +219,6 @@
"build-locales" "build-locales"
] ]
}, },
"build-locales:build": {
"command": "lit-localize build"
},
"build-locales:repair": {
"command": "prettier --write ./src/locale-codes.ts"
},
"build-locales": { "build-locales": {
"command": "node scripts/build-locales.mjs", "command": "node scripts/build-locales.mjs",
"files": [ "files": [
@ -376,9 +372,5 @@
"lint:types" "lint:types"
] ]
} }
}, }
"workspaces": [
".",
"./packages/*"
]
} }

View File

@ -1,3 +1,6 @@
/**
* @file Simple Flow Executor entry point.
*/
import { fromByteArray } from "base64-js"; import { fromByteArray } from "base64-js";
import "formdata-polyfill"; import "formdata-polyfill";
import $ from "jquery"; import $ from "jquery";
@ -33,82 +36,6 @@ function ak(): GlobalAuthentik {
).authentik; ).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 { export interface FlowInfoChallenge {
flowInfo?: ContextualFlowInfo; flowInfo?: ContextualFlowInfo;
responseErrors?: { responseErrors?: {
@ -271,13 +198,10 @@ export interface AuthAssertion {
} }
class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge> { class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge> {
deviceChallenge?: DeviceChallenge; declare deviceChallenge?: DeviceChallenge;
b64enc(buf: Uint8Array): string { b64enc(buf: Uint8Array): string {
return fromByteArray(buf) return fromByteArray(buf).replace(/\+/g, "-").replace(/\//g, "_").replace(/[=]/g, "");
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=/g, "");
} }
b64RawEnc(buf: Uint8Array): string { b64RawEnc(buf: Uint8Array): string {
@ -285,9 +209,8 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
} }
u8arr(input: string): Uint8Array { u8arr(input: string): Uint8Array {
return Uint8Array.from( return Uint8Array.from(atob(input.replace(/_/g, "/").replace(/-/g, "+")), (c) =>
atob(input.replace(/_/g, "/").replace(/-/g, "+")), c.charCodeAt(0),
(c) => c.charCodeAt(0),
); );
} }
@ -295,13 +218,8 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
if ("credentials" in navigator) { if ("credentials" in navigator) {
return true; return true;
} }
if ( if (window.location.protocol === "http:" && window.location.hostname !== "localhost") {
window.location.protocol === "http:" && console.warn("WebAuthn requires this page to be accessed via HTTPS.");
window.location.hostname !== "localhost"
) {
console.warn(
"WebAuthn requires this page to be accessed via HTTPS.",
);
return false; return false;
} }
console.warn("WebAuthn not supported by browser."); 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. // string, then a byte array, re-encode it and wrap that in an array.
const stringId = decodeURIComponent(window.atob(userId)); const stringId = decodeURIComponent(window.atob(userId));
user.id = this.u8arr(this.b64enc(this.u8arr(stringId))); user.id = this.u8arr(this.b64enc(this.u8arr(stringId)));
const challenge = this.u8arr( const challenge = this.u8arr(credentialCreateOptions.challenge.toString());
credentialCreateOptions.challenge.toString(),
);
return Object.assign({}, credentialCreateOptions, { return Object.assign({}, credentialCreateOptions, {
challenge, challenge,
@ -337,28 +253,19 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
* for posting to the server. * for posting to the server.
* @param {PublicKeyCredential} newAssertion * @param {PublicKeyCredential} newAssertion
*/ */
transformNewAssertionForServer( transformNewAssertionForServer(newAssertion: PublicKeyCredential): Assertion {
newAssertion: PublicKeyCredential,
): Assertion {
const attObj = new Uint8Array( const attObj = new Uint8Array(
( (newAssertion.response as AuthenticatorAttestationResponse).attestationObject,
newAssertion.response as AuthenticatorAttestationResponse
).attestationObject,
);
const clientDataJSON = new Uint8Array(
newAssertion.response.clientDataJSON,
); );
const clientDataJSON = new Uint8Array(newAssertion.response.clientDataJSON);
const rawId = new Uint8Array(newAssertion.rawId); const rawId = new Uint8Array(newAssertion.rawId);
const registrationClientExtensions = const registrationClientExtensions = newAssertion.getClientExtensionResults();
newAssertion.getClientExtensionResults();
return { return {
id: newAssertion.id, id: newAssertion.id,
rawId: this.b64enc(rawId), rawId: this.b64enc(rawId),
type: newAssertion.type, type: newAssertion.type,
registrationClientExtensions: JSON.stringify( registrationClientExtensions: JSON.stringify(registrationClientExtensions),
registrationClientExtensions,
),
response: { response: {
clientDataJSON: this.b64enc(clientDataJSON), clientDataJSON: this.b64enc(clientDataJSON),
attestationObject: this.b64enc(attObj), attestationObject: this.b64enc(attObj),
@ -369,16 +276,14 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
transformCredentialRequestOptions( transformCredentialRequestOptions(
credentialRequestOptions: PublicKeyCredentialRequestOptions, credentialRequestOptions: PublicKeyCredentialRequestOptions,
): PublicKeyCredentialRequestOptions { ): PublicKeyCredentialRequestOptions {
const challenge = this.u8arr( const challenge = this.u8arr(credentialRequestOptions.challenge.toString());
credentialRequestOptions.challenge.toString(),
);
const allowCredentials = ( const allowCredentials = (credentialRequestOptions.allowCredentials || []).map(
credentialRequestOptions.allowCredentials || [] (credentialDescriptor) => {
).map((credentialDescriptor) => { const id = this.u8arr(credentialDescriptor.id.toString());
const id = this.u8arr(credentialDescriptor.id.toString()); return Object.assign({}, credentialDescriptor, { id });
return Object.assign({}, credentialDescriptor, { id }); },
}); );
return Object.assign({}, credentialRequestOptions, { return Object.assign({}, credentialRequestOptions, {
challenge, challenge,
@ -390,25 +295,19 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
* Encodes the binary data in the assertion into strings for posting to the server. * Encodes the binary data in the assertion into strings for posting to the server.
* @param {PublicKeyCredential} newAssertion * @param {PublicKeyCredential} newAssertion
*/ */
transformAssertionForServer( transformAssertionForServer(newAssertion: PublicKeyCredential): AuthAssertion {
newAssertion: PublicKeyCredential, const response = newAssertion.response as AuthenticatorAssertionResponse;
): AuthAssertion {
const response =
newAssertion.response as AuthenticatorAssertionResponse;
const authData = new Uint8Array(response.authenticatorData); const authData = new Uint8Array(response.authenticatorData);
const clientDataJSON = new Uint8Array(response.clientDataJSON); const clientDataJSON = new Uint8Array(response.clientDataJSON);
const rawId = new Uint8Array(newAssertion.rawId); const rawId = new Uint8Array(newAssertion.rawId);
const sig = new Uint8Array(response.signature); const sig = new Uint8Array(response.signature);
const assertionClientExtensions = const assertionClientExtensions = newAssertion.getClientExtensionResults();
newAssertion.getClientExtensionResults();
return { return {
id: newAssertion.id, id: newAssertion.id,
rawId: this.b64enc(rawId), rawId: this.b64enc(rawId),
type: newAssertion.type, type: newAssertion.type,
assertionClientExtensions: JSON.stringify( assertionClientExtensions: JSON.stringify(assertionClientExtensions),
assertionClientExtensions,
),
response: { response: {
clientDataJSON: this.b64RawEnc(clientDataJSON), clientDataJSON: this.b64RawEnc(clientDataJSON),
@ -421,8 +320,10 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
render() { render() {
if (!this.deviceChallenge) { if (!this.deviceChallenge) {
return this.renderChallengePicker(); this.renderChallengePicker();
return;
} }
switch (this.deviceChallenge.deviceClass) { switch (this.deviceChallenge.deviceClass) {
case "static": case "static":
case "totp": case "totp":
@ -437,12 +338,10 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
} }
renderChallengePicker() { renderChallengePicker() {
const challenges = this.challenge.deviceChallenges.filter( const challenges = this.challenge.deviceChallenges.filter((challenge) =>
(challenge) => challenge.deviceClass === "webauthn" && !this.checkWebAuthnSupport()
challenge.deviceClass === "webauthn" && ? undefined
!this.checkWebAuthnSupport() : challenge,
? undefined
: challenge,
); );
this.html(`<form id="picker-form"> this.html(`<form id="picker-form">
<img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt=""> <img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
@ -480,12 +379,13 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
.join("")} .join("")}
</form>`); </form>`);
this.challenge.deviceChallenges.forEach((challenge) => { this.challenge.deviceChallenges.forEach((challenge) => {
$( $(`#picker-form button#${challenge.deviceClass}-${challenge.deviceUid}`).on(
`#picker-form button#${challenge.deviceClass}-${challenge.deviceUid}`, "click",
).on("click", () => { () => {
this.deviceChallenge = challenge; this.deviceChallenge = challenge;
this.render(); this.render();
}); },
);
}); });
} }
@ -523,8 +423,7 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
navigator.credentials navigator.credentials
.get({ .get({
publicKey: this.transformCredentialRequestOptions( publicKey: this.transformCredentialRequestOptions(
this.deviceChallenge this.deviceChallenge?.challenge as PublicKeyCredentialRequestOptions,
?.challenge as PublicKeyCredentialRequestOptions,
), ),
}) })
.then((assertion) => { .then((assertion) => {
@ -534,19 +433,16 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
try { try {
// we now have an authentication assertion! encode the byte arrays contained // we now have an authentication assertion! encode the byte arrays contained
// in the assertion data as strings for posting to the server // in the assertion data as strings for posting to the server
const transformedAssertionForServer = const transformedAssertionForServer = this.transformAssertionForServer(
this.transformAssertionForServer( assertion as PublicKeyCredential,
assertion as PublicKeyCredential, );
);
// post the assertion to the server for verification. // post the assertion to the server for verification.
this.executor.submit({ this.executor.submit({
webauthn: transformedAssertionForServer, webauthn: transformedAssertionForServer,
}); });
} catch (err) { } catch (err) {
throw new Error( throw new Error(`Error when validating assertion on server: ${err}`);
`Error when validating assertion on server: ${err}`,
);
} }
}) })
.catch((error) => { .catch((error) => {
@ -557,7 +453,87 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
} }
} }
const sfe = new SimpleFlowExecutor( class SimpleFlowExecutor {
$("#flow-sfe-container")[0] as HTMLDivElement, 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(); sfe.start();

View File

@ -1,8 +1,8 @@
{ {
"extends": "@goauthentik/tsconfig", "extends": "@goauthentik/tsconfig",
"compilerOptions": { "compilerOptions": {
"types": ["jquery"], "emitDeclarationOnly": true,
"baseUrl": ".",
"esModuleInterop": true, "esModuleInterop": true,
"lib": ["DOM", "ES2015", "ES2017"] "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"; * @file Lit Localize build script.
import path from "path"; *
import process from "process"; * @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 * Attempt to stat a file, returning null if it doesn't exist.
* 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.
*/ */
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) { if (!xlfStat) {
const xliff = path.join("./xliff", `${loc}.xlf`); console.error(`lit-localize expected '${languageCode}.xlf', but XLF file is not present`);
const gened = path.join("./src/locales", `${loc}.ts`);
// 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); process.exit(1);
} }
// If the generated file doesn't exist, of course it's not up to date. const generatedTSFilePath = path.join("./src/locales", `${languageCode}.ts`);
try {
var genedStat = fs.statSync(gened); const generatedTSFilePathStat = await tryStat(generatedTSFilePath);
} catch (_error) {
return false; // 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 return {
// presumed to have been generated by that file and is up-to-date. languageCode,
return genedStat.mtimeMs >= xlfStat.mtimeMs; 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 results = await Promise.all(
const upToDate = localizeRules.targetLocales.reduce( localizeRules.targetLocales.map(generatedFileIsUpToDateWithXliffSource),
(acc, loc) => acc && generatedFileIsUpToDateWithXliffSource(loc),
true,
); );
if (!upToDate) { const pendingBuild = results.some((result) => !result.exists || result.expired);
const status = spawnSync("npm", ["run", "build-locales:build"], { encoding: "utf8" });
// Count all the missing message warnings if (!pendingBuild) {
const counts = status.stderr.split("\n").reduce((acc, line) => { console.log("Local is up-to-date!");
const match = /^([\w-]+) message/.exec(line); process.exit(0);
if (!match) {
return acc;
}
acc.set(match[1], (acc.get(match[1]) || 0) + 1);
return acc;
}, new Map());
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`);
} }
console.log("Locale ./src is up-to-date"); 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
for (const line of status.stderr.split("\n")) {
const match = /^([\w-]+) message/.exec(line);
if (!match) continue;
const count = counts.get(match[1]) || 0;
counts.set(match[1], count + 1);
}
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 { deepmerge } from "deepmerge-ts";
import esbuild from "esbuild"; import esbuild from "esbuild";
import { polyfillNode } from "esbuild-plugin-polyfill-node"; import { polyfillNode } from "esbuild-plugin-polyfill-node";
import findFreePorts from "find-free-ports"; import findFreePorts from "find-free-ports";
import { copyFileSync, mkdirSync, readFileSync, statSync } from "fs"; import { copyFileSync, mkdirSync, readFileSync, statSync } from "fs";
import { globSync } from "glob"; 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 * 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 { mdxPlugin } from "./esbuild/build-mdx-plugin.mjs";
import { buildObserverPlugin } from "./esbuild/build-observer-plugin.mjs"; import { buildObserverPlugin } from "./esbuild/build-observer-plugin.mjs";
@ -117,6 +117,7 @@ const BASE_ESBUILD_OPTIONS = {
write: true, write: true,
sourcemap: true, sourcemap: true,
minify: NODE_ENV === "production", minify: NODE_ENV === "production",
legalComments: "external",
splitting: true, splitting: true,
treeShaking: true, treeShaking: true,
external: ["*.woff", "*.woff2"], external: ["*.woff", "*.woff2"],

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import * as http from "http"; import * as http from "http";
import path from "path"; import * as path from "node:path";
/** /**
* Serializes a custom event to a text stream. * 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)}`]; 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:error", listener);
dispatcher.addEventListener("esbuild:end", 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", () => { req.on("close", () => {
console.log("🔌 Client disconnected"); console.log("🔌 Client disconnected");
@ -79,13 +86,6 @@ export function buildObserverPlugin({ serverURL, logPrefix, relativeRoot }) {
dispatcher.removeEventListener("esbuild:error", listener); dispatcher.removeEventListener("esbuild:error", listener);
dispatcher.removeEventListener("esbuild:end", 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 { 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 pseudolocale from "pseudolocale";
import { fileURLToPath } from "url";
import { makeFormatter } from "@lit/localize-tools/lib/formatters/index.js"; import { makeFormatter } from "@lit/localize-tools/lib/formatters/index.js";
import { sortProgramMessages } from "@lit/localize-tools/lib/messages.js"; import { sortProgramMessages } from "@lit/localize-tools/lib/messages.js";
import { TransformLitLocalizer } from "@lit/localize-tools/lib/modes/transform.js"; import { TransformLitLocalizer } from "@lit/localize-tools/lib/modes/transform.js";
const __dirname = fileURLToPath(new URL(".", import.meta.url)); const __dirname = fileURLToPath(new URL(".", import.meta.url));
const pseudoLocale = "pseudo-LOCALE"; const pseudoLocale = /** @type {Locale} */ ("pseudo-LOCALE");
const targetLocales = [pseudoLocale]; 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 // 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 // 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 // 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. // exploitation of the lit/localize-tools internals.
/**
* @type {TransformerConfig}
*/
const config = { const config = {
...baseConfig, ...baseConfig,
baseDir: path.join(__dirname, ".."), baseDir: path.join(__dirname, ".."),
@ -25,18 +50,26 @@ const config = {
...baseConfig.output, ...baseConfig.output,
mode: "transform", mode: "transform",
}, },
resolve: (path) => path, resolve: (filePath) => filePath,
}; };
const pseudoMessagify = (message) => ({ /**
name: message.name, * @param {ProgramMessage} message
contents: message.contents.map((content) => * @returns {Message}
typeof content === "string" ? pseudolocale(content, { prepend: "", append: "" }) : content, */
), function pseudoMessagify({ name, contents }) {
}); return {
name,
contents: contents.map((content) =>
typeof content === "string"
? pseudolocale(content, { prepend: "", append: "" })
: content,
),
};
}
const localizer = new TransformLitLocalizer(config); const localizer = new TransformLitLocalizer(config);
const messages = localizer.extractSourceMessages().messages; const { messages } = localizer.extractSourceMessages();
const translations = messages.map(pseudoMessagify); const translations = messages.map(pseudoMessagify);
const sorted = sortProgramMessages([...messages]); const sorted = sortProgramMessages([...messages]);
const formatter = makeFormatter(config); const formatter = makeFormatter(config);

View File

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

View File

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

View File

@ -46,7 +46,7 @@ export class SystemStatusCard extends AdminStatusCard<SystemInfo> {
return; return;
} }
const outpost = outposts.results[0]; 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({ await new OutpostsApi(DEFAULT_CONFIG).outpostsInstancesUpdate({
uuid: outpost.pk, uuid: outpost.pk,
outpostRequest: outpost, outpostRequest: outpost,

View File

@ -28,16 +28,18 @@ export class WorkersStatusCard extends AdminStatusCard<Worker[]> {
icon: "fa fa-times-circle pf-m-danger", icon: "fa fa-times-circle pf-m-danger",
message: html`${msg("No workers connected. Background tasks will not run.")}`, 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>({ return Promise.resolve<AdminStatus>({
icon: "fa fa-times-circle pf-m-danger", icon: "fa fa-times-circle pf-m-danger",
message: html`${msg("Worker with incorrect version connected.")}`, message: html`${msg("Worker with incorrect version connected.")}`,
}); });
} else {
return Promise.resolve<AdminStatus>({
icon: "fa fa-check-circle pf-m-success",
});
} }
return Promise.resolve<AdminStatus>({
icon: "fa fa-check-circle pf-m-success",
});
} }
renderValue() { renderValue() {

View File

@ -127,7 +127,7 @@ export class SyncStatusChart extends AKChart<SummarizedSyncStatus[]> {
msg("LDAP Source"), 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; return statuses;
} }

View File

@ -6,26 +6,26 @@ import { html } from "lit";
import "../AdminSettingsFooterLinks.js"; import "../AdminSettingsFooterLinks.js";
describe("ak-admin-settings-footer-link", () => { describe("ak-admin-settings-footer-link", () => {
afterEach(async () => { afterEach(() => {
await browser.execute(async () => { return browser.execute(() => {
await document.body.querySelector("ak-admin-settings-footer-link")?.remove(); document.body.querySelector("ak-admin-settings-footer-link")?.remove();
if (document.body["_$litPart$"]) {
// @ts-expect-error expression of type '"_$litPart$"' is added by Lit if ("_$litPart$" in document.body) {
await delete document.body["_$litPart$"]; delete document.body._$litPart$;
} }
}); });
}); });
it("should render an empty control", async () => { it("should render an empty control", async () => {
render(html`<ak-admin-settings-footer-link name="link"></ak-admin-settings-footer-link>`); 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("isValid")).toStrictEqual(false);
await expect(await link.getProperty("toJson")).toEqual({ name: "", href: "" }); await expect(await link.getProperty("toJson")).toEqual({ name: "", href: "" });
}); });
it("should not be valid if just a name is filled in", async () => { 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>`); 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="name"]').setValue("foo");
await expect(await link.getProperty("isValid")).toStrictEqual(false); await expect(await link.getProperty("isValid")).toStrictEqual(false);
await expect(await link.getProperty("toJson")).toEqual({ name: "foo", href: "" }); 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 () => { 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>`); 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 link.$('input[name="href"]').setValue("https://foo.com");
await expect(await link.getProperty("isValid")).toStrictEqual(true); await expect(await link.getProperty("isValid")).toStrictEqual(true);
await expect(await link.getProperty("toJson")).toEqual({ 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 () => { it("should be valid if both are filled in", async () => {
render(html`<ak-admin-settings-footer-link name="link"></ak-admin-settings-footer-link>`); 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="name"]').setValue("foo");
await link.$('input[name="href"]').setValue("https://foo.com"); await link.$('input[name="href"]').setValue("https://foo.com");
await expect(await link.getProperty("isValid")).toStrictEqual(true); 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 () => { 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>`); 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="name"]').setValue("foo");
await link.$('input[name="href"]').setValue("never://foo.com"); await link.$('input[name="href"]').setValue("never://foo.com");
await expect(await link.getProperty("toJson")).toEqual({ 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)) { if (this.can(CapabilitiesEnum.CanSaveMedia)) {
const icon = this.getFormFiles()["metaIcon"]; const icon = this.getFormFiles().metaIcon;
if (icon || this.clearIcon) { if (icon || this.clearIcon) {
await new CoreApi(DEFAULT_CONFIG).coreApplicationsSetIconCreate({ await new CoreApi(DEFAULT_CONFIG).coreApplicationsSetIconCreate({
slug: app.slug, slug: app.slug,
@ -117,7 +117,7 @@ export class ApplicationForm extends WithCapabilitiesConfig(ModelForm<Applicatio
if (!(ev instanceof InputEvent) || !ev.target) { if (!(ev instanceof InputEvent) || !ev.target) {
return; return;
} }
this.clearIcon = !!(ev.target as HTMLInputElement).checked; this.clearIcon = Boolean((ev.target as HTMLInputElement).checked);
} }
renderForm(): TemplateResult { renderForm(): TemplateResult {

View File

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

View File

@ -6,7 +6,7 @@ import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm"; import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
import "@goauthentik/elements/forms/Radio"; import "@goauthentik/elements/forms/Radio";
import "@goauthentik/elements/forms/SearchSelect"; import "@goauthentik/elements/forms/SearchSelect";
import YAML from "yaml"; import * as YAML from "yaml";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
import { CSSResult } from "lit"; import { CSSResult } from "lit";
@ -31,9 +31,8 @@ export class ApplicationEntitlementForm extends ModelForm<ApplicationEntitlement
getSuccessMessage(): string { getSuccessMessage(): string {
if (this.instance?.pbmUuid) { if (this.instance?.pbmUuid) {
return msg("Successfully updated entitlement."); return msg("Successfully updated entitlement.");
} else {
return msg("Successfully created entitlement.");
} }
return msg("Successfully created entitlement.");
} }
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
@ -49,11 +48,10 @@ export class ApplicationEntitlementForm extends ModelForm<ApplicationEntitlement
pbmUuid: this.instance.pbmUuid || "", pbmUuid: this.instance.pbmUuid || "",
applicationEntitlementRequest: data, applicationEntitlementRequest: data,
}); });
} else {
return new CoreApi(DEFAULT_CONFIG).coreApplicationEntitlementsCreate({
applicationEntitlementRequest: data,
});
} }
return new CoreApi(DEFAULT_CONFIG).coreApplicationEntitlementsCreate({
applicationEntitlementRequest: data,
});
} }
renderForm(): TemplateResult { renderForm(): TemplateResult {

View File

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

View File

@ -37,7 +37,7 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
errors = new Map<string, string>(); errors = new Map<string, string>();
@query("form#applicationform") @query("form#applicationform")
form!: HTMLFormElement; declare form: HTMLFormElement;
constructor() { constructor() {
super(); super();
@ -61,18 +61,18 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
this.errors = new Map(); this.errors = new Map();
const values = trimMany(this.formValues ?? {}, ["metaLaunchUrl", "name", "slug"]); const values = trimMany(this.formValues ?? {}, ["metaLaunchUrl", "name", "slug"]);
if (values["name"] === "") { if (values.name === "") {
this.errors.set("name", msg("An application name is required")); this.errors.set("name", msg("An application name is required"));
} }
if ( if (
!( !(
isStr(values["metaLaunchUrl"]) && isStr(values.metaLaunchUrl) &&
(values["metaLaunchUrl"] === "" || URL.canParse(values["metaLaunchUrl"])) (values.metaLaunchUrl === "" || URL.canParse(values.metaLaunchUrl))
) )
) { ) {
this.errors.set("metaLaunchUrl", msg("Not a valid URL")); 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")); this.errors.set("slug", msg("Not a valid slug"));
} }
return this.errors.size === 0; return this.errors.size === 0;

View File

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

View File

@ -22,7 +22,7 @@ export class ApplicationWizardProviderSamlForm extends ApplicationWizardProvider
const setHasSigningKp = (ev: InputEvent) => { const setHasSigningKp = (ev: InputEvent) => {
const target = ev.target as AkCryptoCertificateSearch; const target = ev.target as AkCryptoCertificateSearch;
if (!target) return; if (!target) return;
this.hasSigningKp = !!target.selectedKeypair; this.hasSigningKp = Boolean(target.selectedKeypair);
}; };
return html` <ak-wizard-title>${this.label}</ak-wizard-title> 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 "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm"; import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
import "@goauthentik/elements/forms/SearchSelect"; import "@goauthentik/elements/forms/SearchSelect";
import YAML from "yaml"; import * as YAML from "yaml";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, html } from "lit"; import { CSSResult, TemplateResult, html } from "lit";
@ -59,11 +59,10 @@ export class BlueprintForm extends ModelForm<BlueprintInstance, string> {
instanceUuid: this.instance.pk, instanceUuid: this.instance.pk,
blueprintInstanceRequest: data, blueprintInstanceRequest: data,
}); });
} else {
return new ManagedApi(DEFAULT_CONFIG).managedBlueprintsCreate({
blueprintInstanceRequest: data,
});
} }
return new ManagedApi(DEFAULT_CONFIG).managedBlueprintsCreate({
blueprintInstanceRequest: data,
});
} }
renderForm(): TemplateResult { renderForm(): TemplateResult {

View File

@ -9,7 +9,7 @@ import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm"; import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
import "@goauthentik/elements/forms/SearchSelect"; import "@goauthentik/elements/forms/SearchSelect";
import { DefaultBrand } from "@goauthentik/elements/sidebar/SidebarBrand"; import { DefaultBrand } from "@goauthentik/elements/sidebar/SidebarBrand";
import YAML from "yaml"; import * as YAML from "yaml";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit"; import { TemplateResult, html } from "lit";
@ -43,11 +43,10 @@ export class BrandForm extends ModelForm<Brand, string> {
brandUuid: this.instance.brandUuid, brandUuid: this.instance.brandUuid,
brandRequest: data, brandRequest: data,
}); });
} else {
return new CoreApi(DEFAULT_CONFIG).coreBrandsCreate({
brandRequest: data,
});
} }
return new CoreApi(DEFAULT_CONFIG).coreBrandsCreate({
brandRequest: data,
});
} }
renderForm(): TemplateResult { renderForm(): TemplateResult {

View File

@ -107,7 +107,7 @@ export class AkCryptoCertificateSearch extends CustomListenerElement(AKElement)
selected(item: CertificateKeyPair, items: CertificateKeyPair[]) { selected(item: CertificateKeyPair, items: CertificateKeyPair[]) {
return ( return (
(this.singleton && !this.certificate && items.length === 1) || (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: { argTypes: {
// Typescript is unaware that arguments for components are treated as properties, and // 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. // properties are typically renamed to lower case, even if the variable is not.
// @ts-expect-error // @ts-expect-error TODO: Explain.
nokey: { nokey: {
control: "boolean", control: "boolean",
description: description:
@ -75,7 +75,7 @@ export const CryptoCertificateSearch = () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const showMessage = (ev: CustomEvent<any>) => { const showMessage = (ev: CustomEvent<any>) => {
const detail = ev.detail; const detail = ev.detail;
delete detail["target"]; delete detail.target;
document.getElementById("message-pad")!.innerText = `Event: ${JSON.stringify( document.getElementById("message-pad")!.innerText = `Event: ${JSON.stringify(
detail, detail,
null, null,

View File

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

View File

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

View File

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

View File

@ -58,7 +58,7 @@ export class FlowForm extends WithCapabilitiesConfig(ModelForm<Flow, string>) {
} }
if (this.can(CapabilitiesEnum.CanSaveMedia)) { if (this.can(CapabilitiesEnum.CanSaveMedia)) {
const icon = this.getFormFiles()["background"]; const icon = this.getFormFiles().background;
if (icon || this.clearBackground) { if (icon || this.clearBackground) {
await new FlowsApi(DEFAULT_CONFIG).flowsInstancesSetBackgroundCreate({ await new FlowsApi(DEFAULT_CONFIG).flowsInstancesSetBackgroundCreate({
slug: flow.slug, slug: flow.slug,

View File

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

View File

@ -39,9 +39,8 @@ export class StageBindingForm extends ModelForm<FlowStageBinding, string> {
getSuccessMessage(): string { getSuccessMessage(): string {
if (this.instance?.pk) { if (this.instance?.pk) {
return msg("Successfully updated binding."); return msg("Successfully updated binding.");
} else {
return msg("Successfully created binding.");
} }
return msg("Successfully created binding.");
} }
send(data: FlowStageBinding): Promise<unknown> { send(data: FlowStageBinding): Promise<unknown> {
@ -50,14 +49,13 @@ export class StageBindingForm extends ModelForm<FlowStageBinding, string> {
fsbUuid: this.instance.pk, fsbUuid: this.instance.pk,
patchedFlowStageBindingRequest: data, patchedFlowStageBindingRequest: data,
}); });
} else {
if (this.targetPk) {
data.target = this.targetPk;
}
return new FlowsApi(DEFAULT_CONFIG).flowsBindingsCreate({
flowStageBindingRequest: data,
});
} }
if (this.targetPk) {
data.target = this.targetPk;
}
return new FlowsApi(DEFAULT_CONFIG).flowsBindingsCreate({
flowStageBindingRequest: data,
});
} }
async getOrder(): Promise<number> { async getOrder(): Promise<number> {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -32,11 +32,10 @@ export class ServiceConnectionDockerForm extends ModelForm<DockerServiceConnecti
uuid: this.instance.pk || "", uuid: this.instance.pk || "",
dockerServiceConnectionRequest: data, dockerServiceConnectionRequest: data,
}); });
} else {
return new OutpostsApi(DEFAULT_CONFIG).outpostsServiceConnectionsDockerCreate({
dockerServiceConnectionRequest: data,
});
} }
return new OutpostsApi(DEFAULT_CONFIG).outpostsServiceConnectionsDockerCreate({
dockerServiceConnectionRequest: data,
});
} }
renderForm(): TemplateResult { renderForm(): TemplateResult {

View File

@ -4,7 +4,7 @@ import "@goauthentik/elements/CodeMirror";
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror"; import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/forms/HorizontalFormElement"; import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm"; import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
import YAML from "yaml"; import * as YAML from "yaml";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit"; import { TemplateResult, html } from "lit";
@ -36,11 +36,10 @@ export class ServiceConnectionKubernetesForm extends ModelForm<
uuid: this.instance.pk || "", uuid: this.instance.pk || "",
kubernetesServiceConnectionRequest: data, kubernetesServiceConnectionRequest: data,
}); });
} else {
return new OutpostsApi(DEFAULT_CONFIG).outpostsServiceConnectionsKubernetesCreate({
kubernetesServiceConnectionRequest: data,
});
} }
return new OutpostsApi(DEFAULT_CONFIG).outpostsServiceConnectionsKubernetesCreate({
kubernetesServiceConnectionRequest: data,
});
} }
renderForm(): TemplateResult { renderForm(): TemplateResult {

View File

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

View File

@ -72,9 +72,8 @@ export class PolicyBindingForm extends ModelForm<PolicyBinding, string> {
getSuccessMessage(): string { getSuccessMessage(): string {
if (this.instance?.pk) { if (this.instance?.pk) {
return msg("Successfully updated binding."); return msg("Successfully updated binding.");
} else {
return msg("Successfully created binding.");
} }
return msg("Successfully created binding.");
} }
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
@ -111,11 +110,10 @@ export class PolicyBindingForm extends ModelForm<PolicyBinding, string> {
policyBindingUuid: this.instance.pk, policyBindingUuid: this.instance.pk,
policyBindingRequest: data, policyBindingRequest: data,
}); });
} else {
return new PoliciesApi(DEFAULT_CONFIG).policiesBindingsCreate({
policyBindingRequest: data,
});
} }
return new PoliciesApi(DEFAULT_CONFIG).policiesBindingsCreate({
policyBindingRequest: data,
});
} }
async getOrder(): Promise<number> { async getOrder(): Promise<number> {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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