Compare commits

..

14 Commits

Author SHA1 Message Date
2990913f9d website/docs: Remove comment. 2025-05-03 04:06:45 +02:00
550775dc69 Reapply "website/docs: Prepare for monorepo. (#14119) 2025-05-03 04:01:37 +02:00
e473f28e21 web: NPM workspaces (#14274)
docusaurus-config: v1.0.6
2025-05-02 21:52:54 -04:00
f70635c295 web: Clean up browser-only module imports that crash WebDriverIO. (#14330)
* web: Clean up browser-only module imports that crash WebDriverIO.

* web: Clarify slug format output.
2025-05-02 20:04:05 -04:00
70d60c7ab2 web: Use monorepo package utilities to build packages (#14159)
* web: Format live reload package.

* web: Format package.json.

* web: Revise globals.

* web: Build entrypoints with a single ESBuild context. Clean up entrypoints.

* web: WIP Prepare monorepo package for use.

* web: Update build paths. Fix types.

* web: WIP Add monorepo dependency.

* web: Use monorepo utilities when building.

* web: Fix issue where linters collide. Update ignore file.

- Remove unused sort override for polyfills.

* core: Prepare repo for NPM workspaces.
2025-05-02 19:48:19 -04:00
61a26c02b7 Revert-revert: Safari fixes (#14331)
* Reapply "web: Safari fixes merge branch (#14181)"

This reverts commit a41d45834c.

* web: Fix brand preference order. Adjust header height.
2025-05-02 21:26:40 +02:00
a06645d558 website/docs: remove support badge (#14343)
removed support badge1

Co-authored-by: Tana M Berry <tana@goauthentik.io>
2025-05-02 18:15:20 +00:00
7730ecbd37 ci: use dependabot for compose correctly? (#14340)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-05-02 19:21:55 +02:00
80e1be8db7 website/docs: use Universal Device Trust for GDTC instead of Okta (#14335)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-05-02 19:11:14 +02:00
c528c74e48 ci: use dependabot for docker-compose files (#14336)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-05-02 19:05:36 +02:00
6d7bf36afe website/docs: fix dry-run release highlight (#14337)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-05-02 19:05:25 +02:00
44fb59eb18 rbac: fix RoleObjectPermissionTable not showing add_user_to_group (#14312)
fix RoleObjectPermissionTable not showing `add_user_to_group`
2025-05-02 17:42:19 +02:00
8f8d924935 core, web: update translations (#14326)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: rissson <18313093+rissson@users.noreply.github.com>
2025-05-02 17:41:47 +02:00
602adaa5c5 core: bump github.com/sethvargo/go-envconfig from 1.2.0 to 1.3.0 (#14327)
Bumps [github.com/sethvargo/go-envconfig](https://github.com/sethvargo/go-envconfig) from 1.2.0 to 1.3.0.
- [Release notes](https://github.com/sethvargo/go-envconfig/releases)
- [Commits](https://github.com/sethvargo/go-envconfig/compare/v1.2.0...v1.3.0)

---
updated-dependencies:
- dependency-name: github.com/sethvargo/go-envconfig
  dependency-version: 1.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-02 17:30:24 +02:00
175 changed files with 9004 additions and 5680 deletions

View File

@ -118,3 +118,15 @@ updates:
prefix: "core:"
labels:
- dependencies
- package-ecosystem: docker-compose
directories:
# - /scripts # Maybe
- /tests/e2e
schedule:
interval: daily
time: "04:00"
open-pull-requests-limit: 10
commit-message:
prefix: "core:"
labels:
- dependencies

View File

@ -16,7 +16,7 @@
],
"typescript.preferences.importModuleSpecifier": "non-relative",
"typescript.preferences.importModuleSpecifierEnding": "index",
"typescript.tsdk": "./web/node_modules/typescript/lib",
"typescript.tsdk": "./node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true,
"yaml.schemas": {
"./blueprints/schema.json": "blueprints/**/*.yaml"
@ -30,7 +30,5 @@
}
],
"go.testFlags": ["-count=1"],
"github-actions.workflows.pinned.workflows": [
".github/workflows/ci-main.yml"
]
"github-actions.workflows.pinned.workflows": [".github/workflows/ci-main.yml"]
}

2
go.mod
View File

@ -22,7 +22,7 @@ require (
github.com/pires/go-proxyproto v0.8.0
github.com/prometheus/client_golang v1.22.0
github.com/redis/go-redis/v9 v9.8.0
github.com/sethvargo/go-envconfig v1.2.0
github.com/sethvargo/go-envconfig v1.3.0
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.9.1
github.com/stretchr/testify v1.10.0

4
go.sum
View File

@ -251,8 +251,8 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sethvargo/go-envconfig v1.2.0 h1:q3XkOZWkC+G1sMLCrw9oPGTjYexygLOXDmGUit1ti8Q=
github.com/sethvargo/go-envconfig v1.2.0/go.mod h1:JLd0KFWQYzyENqnEPWWZ49i4vzZo/6nRidxI8YvGiHw=
github.com/sethvargo/go-envconfig v1.3.0 h1:gJs+Fuv8+f05omTpwWIu6KmuseFAXKrIaOZSh8RMt0U=
github.com/sethvargo/go-envconfig v1.3.0/go.mod h1:JLd0KFWQYzyENqnEPWWZ49i4vzZo/6nRidxI8YvGiHw=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=

Binary file not shown.

538
package-lock.json generated
View File

@ -1,12 +1,546 @@
{
"name": "@goauthentik/authentik",
"version": "2025.2.1",
"version": "2025.4.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@goauthentik/authentik",
"version": "2025.2.1"
"version": "2025.4.0",
"devDependencies": {
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
"prettier": "^3.3.3",
"prettier-plugin-organize-imports": "^4.1.0",
"prettier-plugin-packagejson": "^2.5.10",
"typescript": "^5.6.2"
}
},
"node_modules/@babel/code-frame": {
"version": "7.26.2",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
"integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-validator-identifier": "^7.25.9",
"js-tokens": "^4.0.0",
"picocolors": "^1.0.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/generator": {
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.0.tgz",
"integrity": "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.27.0",
"@babel/types": "^7.27.0",
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.25",
"jsesc": "^3.0.2"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-string-parser": {
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
"integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
"integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz",
"integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.27.0"
},
"bin": {
"parser": "bin/babel-parser.js"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@babel/template": {
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz",
"integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.26.2",
"@babel/parser": "^7.27.0",
"@babel/types": "^7.27.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/traverse": {
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.0.tgz",
"integrity": "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.26.2",
"@babel/generator": "^7.27.0",
"@babel/parser": "^7.27.0",
"@babel/template": "^7.27.0",
"@babel/types": "^7.27.0",
"debug": "^4.3.1",
"globals": "^11.1.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/types": {
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz",
"integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.25.9",
"@babel/helper-validator-identifier": "^7.25.9"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.8",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
"integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/set-array": "^1.2.1",
"@jridgewell/sourcemap-codec": "^1.4.10",
"@jridgewell/trace-mapping": "^0.3.24"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/set-array": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.25",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@pkgr/core": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.2.tgz",
"integrity": "sha512-fdDH1LSGfZdTH2sxdpVMw31BanV28K/Gry0cVFxaNP77neJSkd82mM8ErPNYs9e+0O7SdHBLTDzDgwUuy18RnQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^12.20.0 || ^14.18.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/unts"
}
},
"node_modules/@trivago/prettier-plugin-sort-imports": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-5.2.2.tgz",
"integrity": "sha512-fYDQA9e6yTNmA13TLVSA+WMQRc5Bn/c0EUBditUHNfMMxN7M82c38b1kEggVE3pLpZ0FwkwJkUEKMiOi52JXFA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@babel/generator": "^7.26.5",
"@babel/parser": "^7.26.7",
"@babel/traverse": "^7.26.7",
"@babel/types": "^7.26.7",
"javascript-natural-sort": "^0.7.1",
"lodash": "^4.17.21"
},
"engines": {
"node": ">18.12"
},
"peerDependencies": {
"@vue/compiler-sfc": "3.x",
"prettier": "2.x - 3.x",
"prettier-plugin-svelte": "3.x",
"svelte": "4.x || 5.x"
},
"peerDependenciesMeta": {
"@vue/compiler-sfc": {
"optional": true
},
"prettier-plugin-svelte": {
"optional": true
},
"svelte": {
"optional": true
}
}
},
"node_modules/debug": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/detect-indent": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-7.0.1.tgz",
"integrity": "sha512-Mc7QhQ8s+cLrnUfU/Ji94vG/r8M26m8f++vyres4ZoojaRDpZ1eSIh/EpzLNwlWuvzSZ3UbDFspjFvTDXe6e/g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12.20"
}
},
"node_modules/detect-newline": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-4.0.1.tgz",
"integrity": "sha512-qE3Veg1YXzGHQhlA6jzebZN2qVf6NX+A7m7qlhCGG30dJixrAQhYOsJjsnBjJkCSmuOPpCk30145fr8FV0bzog==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/fdir": {
"version": "6.4.4",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
"integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"picomatch": "^3 || ^4"
},
"peerDependenciesMeta": {
"picomatch": {
"optional": true
}
}
},
"node_modules/get-stdin": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-9.0.0.tgz",
"integrity": "sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/git-hooks-list": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/git-hooks-list/-/git-hooks-list-3.2.0.tgz",
"integrity": "sha512-ZHG9a1gEhUMX1TvGrLdyWb9kDopCBbTnI8z4JgRMYxsijWipgjSEYoPWqBuIB0DnRnvqlQSEeVmzpeuPm7NdFQ==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/fisker/git-hooks-list?sponsor=1"
}
},
"node_modules/globals": {
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/is-plain-obj": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
"integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/javascript-natural-sort": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz",
"integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==",
"dev": true,
"license": "MIT"
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true,
"license": "MIT"
},
"node_modules/jsesc": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
"dev": true,
"license": "MIT",
"bin": {
"jsesc": "bin/jsesc"
},
"engines": {
"node": ">=6"
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true,
"license": "MIT"
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"license": "MIT"
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true,
"license": "ISC"
},
"node_modules/picomatch": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/prettier": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
"dev": true,
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/prettier-plugin-organize-imports": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.1.0.tgz",
"integrity": "sha512-5aWRdCgv645xaa58X8lOxzZoiHAldAPChljr/MT0crXVOWTZ+Svl4hIWlz+niYSlO6ikE5UXkN1JrRvIP2ut0A==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"prettier": ">=2.0",
"typescript": ">=2.9",
"vue-tsc": "^2.1.0"
},
"peerDependenciesMeta": {
"vue-tsc": {
"optional": true
}
}
},
"node_modules/prettier-plugin-packagejson": {
"version": "2.5.10",
"resolved": "https://registry.npmjs.org/prettier-plugin-packagejson/-/prettier-plugin-packagejson-2.5.10.tgz",
"integrity": "sha512-LUxATI5YsImIVSaaLJlJ3aE6wTD+nvots18U3GuQMJpUyClChaZlQrqx3dBnbhF20OnKWZyx8EgyZypQtBDtgQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"sort-package-json": "2.15.1",
"synckit": "0.9.2"
},
"peerDependencies": {
"prettier": ">= 1.16.0"
},
"peerDependenciesMeta": {
"prettier": {
"optional": true
}
}
},
"node_modules/semver": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/sort-object-keys": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/sort-object-keys/-/sort-object-keys-1.1.3.tgz",
"integrity": "sha512-855pvK+VkU7PaKYPc+Jjnmt4EzejQHyhhF33q31qG8x7maDzkeFhAAThdCYay11CISO+qAMwjOBP+fPZe0IPyg==",
"dev": true,
"license": "MIT"
},
"node_modules/sort-package-json": {
"version": "2.15.1",
"resolved": "https://registry.npmjs.org/sort-package-json/-/sort-package-json-2.15.1.tgz",
"integrity": "sha512-9x9+o8krTT2saA9liI4BljNjwAbvUnWf11Wq+i/iZt8nl2UGYnf3TH5uBydE7VALmP7AGwlfszuEeL8BDyb0YA==",
"dev": true,
"license": "MIT",
"dependencies": {
"detect-indent": "^7.0.1",
"detect-newline": "^4.0.0",
"get-stdin": "^9.0.0",
"git-hooks-list": "^3.0.0",
"is-plain-obj": "^4.1.0",
"semver": "^7.6.0",
"sort-object-keys": "^1.1.3",
"tinyglobby": "^0.2.9"
},
"bin": {
"sort-package-json": "cli.js"
}
},
"node_modules/synckit": {
"version": "0.9.2",
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.2.tgz",
"integrity": "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@pkgr/core": "^0.1.0",
"tslib": "^2.6.2"
},
"engines": {
"node": "^14.18.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/unts"
}
},
"node_modules/tinyglobby": {
"version": "0.2.13",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz",
"integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==",
"dev": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.4.4",
"picomatch": "^4.0.2"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"dev": true,
"license": "0BSD"
},
"node_modules/typescript": {
"version": "5.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
}
}
}

View File

@ -1,5 +1,15 @@
{
"name": "@goauthentik/authentik",
"version": "2025.4.0",
"private": true
"private": true,
"type": "module",
"devDependencies": {
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
"prettier": "^3.3.3",
"prettier-plugin-organize-imports": "^4.1.0",
"prettier-plugin-packagejson": "^2.5.10",
"typescript": "^5.6.2"
},
"workspaces": [],
"prettier": "./packages/prettier-config/index.js"
}

View File

@ -1,12 +1,12 @@
{
"name": "@goauthentik/docusaurus-config",
"version": "1.0.5",
"version": "1.0.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@goauthentik/docusaurus-config",
"version": "1.0.5",
"version": "1.0.6",
"license": "MIT",
"dependencies": {
"deepmerge-ts": "^7.1.5",

View File

@ -1,6 +1,6 @@
{
"name": "@goauthentik/docusaurus-config",
"version": "1.0.5",
"version": "1.0.6",
"description": "authentik's Docusaurus config",
"license": "MIT",
"scripts": {

28
tsconfig.json Normal file
View File

@ -0,0 +1,28 @@
// TypeScript Project Configuration
{
"extends": "./packages/tsconfig/tsconfig.json",
"compilerOptions": {
"baseUrl": "."
},
"watchOptions": {
"excludeDirectories": [
"**/.git", // Git
"**/.yarn", // Yarn
"**/.vscode", // VS Code
"**/.vscode-test-web", // VS Code Web Test
"**/dist", // Distributed build files
"**/out", // Output build files
"**/.drafts", // Drafts
"**/.github", // GitHub
"**/node_modules" // Node modules
]
},
// The root project has no sources of its own. By setting `files` to an empty
// list, TS won't automatically include all sources below root (the default).
"files": [],
"references": [
// Note that references are in the order we want them to be built.
// TODO: Left blank until TypeScript workspaces are complete.
]
}

55
web/package-lock.json generated
View File

@ -25,7 +25,6 @@
"@formatjs/intl-listformat": "^7.5.7",
"@fortawesome/fontawesome-free": "^6.6.0",
"@goauthentik/api": "^2025.4.0-1746018955",
"@lit-labs/ssr": "3.2.2",
"@lit/context": "^1.1.2",
"@lit/localize": "^0.12.2",
"@lit/reactive-element": "^2.0.4",
@ -66,6 +65,7 @@
"remark-gfm": "^4.0.1",
"remark-mdx-frontmatter": "^5.0.0",
"style-mod": "^4.1.2",
"trusted-types": "^2.0.0",
"ts-pattern": "^5.4.0",
"unist-util-visit": "^5.0.0",
"webcomponent-qr-code": "^1.2.0",
@ -2322,50 +2322,11 @@
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@lit-labs/ssr": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@lit-labs/ssr/-/ssr-3.2.2.tgz",
"integrity": "sha512-He5TzeNPM9ECmVpgXRYmVlz0UA5YnzHlT43kyLi2Lu6mUidskqJVonk9W5K699+2DKhoXp8Ra4EJmHR6KrcW1Q==",
"license": "BSD-3-Clause",
"dependencies": {
"@lit-labs/ssr-client": "^1.1.7",
"@lit-labs/ssr-dom-shim": "^1.2.0",
"@lit/reactive-element": "^2.0.4",
"@parse5/tools": "^0.3.0",
"@types/node": "^16.0.0",
"enhanced-resolve": "^5.10.0",
"lit": "^3.1.2",
"lit-element": "^4.0.4",
"lit-html": "^3.1.2",
"node-fetch": "^3.2.8",
"parse5": "^7.1.1"
},
"engines": {
"node": ">=13.9.0"
}
},
"node_modules/@lit-labs/ssr-client": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@lit-labs/ssr-client/-/ssr-client-1.1.7.tgz",
"integrity": "sha512-VvqhY/iif3FHrlhkzEPsuX/7h/NqnfxLwVf0p8ghNIlKegRyRqgeaJevZ57s/u/LiFyKgqksRP5n+LmNvpxN+A==",
"license": "BSD-3-Clause",
"dependencies": {
"@lit/reactive-element": "^2.0.4",
"lit": "^3.1.2",
"lit-html": "^3.1.2"
}
},
"node_modules/@lit-labs/ssr-dom-shim": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.2.1.tgz",
"integrity": "sha512-wx4aBmgeGvFmOKucFKY+8VFJSYZxs9poN3SDNQFF6lT6NrQUnHiPB2PWz2sc4ieEcAaYYzN+1uWahEeTq2aRIQ=="
},
"node_modules/@lit-labs/ssr/node_modules/@types/node": {
"version": "16.18.114",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.114.tgz",
"integrity": "sha512-7oAtnxrgkMNzyzT443UDWwzkmYew81F1ZSPm3/lsITJfW/WludaSOpegTvUG+UdapcbrtWOtY/E4LyTkhPGJ5Q==",
"license": "MIT"
},
"node_modules/@lit/context": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@lit/context/-/context-1.1.2.tgz",
@ -2975,6 +2936,7 @@
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@parse5/tools/-/tools-0.3.0.tgz",
"integrity": "sha512-zxRyTHkqb7WQMV8kTNBKWb1BeOFUKXBXTBWuxg9H9hfvQB3IwP6Iw2U75Ia5eyRxPNltmY7E8YAlz6zWwUnjKg==",
"dev": true,
"dependencies": {
"parse5": "^7.0.0"
}
@ -10822,6 +10784,7 @@
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
"dev": true,
"engines": {
"node": ">= 12"
}
@ -11472,6 +11435,7 @@
"version": "5.17.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz",
"integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==",
"dev": true,
"dependencies": {
"graceful-fs": "^4.2.4",
"tapable": "^2.2.0"
@ -14023,7 +13987,8 @@
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"dev": true
},
"node_modules/grapheme-splitter": {
"version": "1.0.4",
@ -18741,6 +18706,7 @@
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
"dev": true,
"dependencies": {
"data-uri-to-buffer": "^4.0.0",
"fetch-blob": "^3.1.4",
@ -22761,6 +22727,7 @@
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
"integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
"dev": true,
"engines": {
"node": ">=6"
}
@ -23152,6 +23119,12 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/trusted-types": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/trusted-types/-/trusted-types-2.0.0.tgz",
"integrity": "sha512-Eam+AUp6lg04YjmYkuLNhEJX+6ByocrKTpY/TtfRK/gV6OmxeN0OwkIasor28SUJ606snArpPLGtPMGbqdaaUA==",
"license": "W3C-20150513"
},
"node_modules/ts-api-utils": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz",

View File

@ -51,7 +51,6 @@
"@formatjs/intl-listformat": "^7.5.7",
"@fortawesome/fontawesome-free": "^6.6.0",
"@goauthentik/api": "^2025.4.0-1746018955",
"@lit-labs/ssr": "3.2.2",
"@lit/context": "^1.1.2",
"@lit/localize": "^0.12.2",
"@lit/reactive-element": "^2.0.4",
@ -92,6 +91,7 @@
"remark-gfm": "^4.0.1",
"remark-mdx-frontmatter": "^5.0.0",
"style-mod": "^4.1.2",
"trusted-types": "^2.0.0",
"ts-pattern": "^5.4.0",
"unist-util-visit": "^5.0.0",
"webcomponent-qr-code": "^1.2.0",

View File

@ -1,11 +1,11 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { VERSION } from "@goauthentik/common/constants";
import { globalAK } from "@goauthentik/common/global";
import { DefaultBrand } from "@goauthentik/common/ui/config";
import "@goauthentik/elements/EmptyState";
import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider";
import { WithLicenseSummary } from "@goauthentik/elements/Interface/licenseSummaryProvider";
import { ModalButton } from "@goauthentik/elements/buttons/ModalButton";
import { DefaultBrand } from "@goauthentik/elements/sidebar/SidebarBrand";
import { msg } from "@lit/localize";
import { TemplateResult, css, html } from "lit";

View File

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

View File

@ -4,13 +4,17 @@ import { ROUTES } from "@goauthentik/admin/Routes";
import {
EVENT_API_DRAWER_TOGGLE,
EVENT_NOTIFICATION_DRAWER_TOGGLE,
EVENT_SIDEBAR_TOGGLE,
} from "@goauthentik/common/constants";
import { configureSentry } from "@goauthentik/common/sentry";
import { me } from "@goauthentik/common/users";
import { WebsocketClient } from "@goauthentik/common/ws";
import { AuthenticatedInterface } from "@goauthentik/elements/Interface";
import { WithLicenseSummary } from "@goauthentik/elements/Interface/licenseSummaryProvider.js";
import "@goauthentik/elements/ak-locale-context";
import "@goauthentik/elements/banner/EnterpriseStatusBanner";
import "@goauthentik/elements/banner/EnterpriseStatusBanner";
import "@goauthentik/elements/banner/VersionBanner";
import "@goauthentik/elements/banner/VersionBanner";
import "@goauthentik/elements/messages/MessageContainer";
import "@goauthentik/elements/messages/MessageContainer";
@ -21,25 +25,32 @@ import "@goauthentik/elements/router/RouterOutlet";
import "@goauthentik/elements/sidebar/Sidebar";
import "@goauthentik/elements/sidebar/SidebarItem";
import { CSSResult, TemplateResult, css, html } from "lit";
import { CSSResult, TemplateResult, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators.js";
import { classMap } from "lit/directives/class-map.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFDrawer from "@patternfly/patternfly/components/Drawer/drawer.css";
import PFNav from "@patternfly/patternfly/components/Nav/nav.css";
import PFPage from "@patternfly/patternfly/components/Page/page.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { SessionUser, UiThemeEnum } from "@goauthentik/api";
import { LicenseSummaryStatusEnum, SessionUser, UiThemeEnum } from "@goauthentik/api";
import "./AdminSidebar";
import {
AdminSidebarEnterpriseEntries,
AdminSidebarEntries,
renderSidebarItems,
} from "./AdminSidebar.js";
if (process.env.NODE_ENV === "development") {
await import("@goauthentik/esbuild-plugin-live-reload/client");
}
@customElement("ak-interface-admin")
export class AdminInterface extends AuthenticatedInterface {
export class AdminInterface extends WithLicenseSummary(AuthenticatedInterface) {
//#region Properties
@property({ type: Boolean })
notificationDrawerOpen = getURLParam("notificationDrawerOpen", false);
@ -54,12 +65,29 @@ export class AdminInterface extends AuthenticatedInterface {
@query("ak-about-modal")
aboutModal?: AboutModal;
@property({ type: Boolean, reflect: true })
public sidebarOpen: boolean;
#toggleSidebar = () => {
this.sidebarOpen = !this.sidebarOpen;
};
#sidebarMatcher: MediaQueryList;
#sidebarListener = (event: MediaQueryListEvent) => {
this.sidebarOpen = event.matches;
};
//#endregion
//#region Styles
static get styles(): CSSResult[] {
return [
PFBase,
PFPage,
PFButton,
PFDrawer,
PFNav,
css`
.pf-c-page__main,
.pf-c-drawer__content,
@ -67,23 +95,30 @@ export class AdminInterface extends AuthenticatedInterface {
z-index: auto !important;
background-color: transparent;
}
.display-none {
display: none;
}
.pf-c-page {
background-color: var(--pf-c-page--BackgroundColor) !important;
}
/* Global page background colour */
:host([theme="dark"]) .pf-c-page {
--pf-c-page--BackgroundColor: var(--ak-dark-background);
:host([theme="dark"]) {
/* Global page background colour */
.pf-c-page {
--pf-c-page--BackgroundColor: var(--ak-dark-background);
}
}
ak-enterprise-status,
ak-version-banner {
ak-page-navbar {
grid-area: header;
}
ak-admin-sidebar {
.ak-sidebar {
grid-area: nav;
}
.pf-c-drawer__panel {
z-index: var(--pf-global--ZIndex--xl);
}
@ -91,10 +126,23 @@ export class AdminInterface extends AuthenticatedInterface {
];
}
//#endregion
//#region Lifecycle
constructor() {
super();
this.ws = new WebsocketClient();
this.#sidebarMatcher = window.matchMedia("(min-width: 1200px)");
this.sidebarOpen = this.#sidebarMatcher.matches;
}
public connectedCallback() {
super.connectedCallback();
window.addEventListener(EVENT_SIDEBAR_TOGGLE, this.#toggleSidebar);
window.addEventListener(EVENT_NOTIFICATION_DRAWER_TOGGLE, () => {
this.notificationDrawerOpen = !this.notificationDrawerOpen;
updateURLParams({
@ -108,6 +156,14 @@ export class AdminInterface extends AuthenticatedInterface {
apiDrawerOpen: this.apiDrawerOpen,
});
});
this.#sidebarMatcher.addEventListener("change", this.#sidebarListener);
}
public disconnectedCallback(): void {
super.disconnectedCallback();
window.removeEventListener(EVENT_SIDEBAR_TOGGLE, this.#toggleSidebar);
this.#sidebarMatcher.removeEventListener("change", this.#sidebarListener);
}
async firstUpdated(): Promise<void> {
@ -118,6 +174,7 @@ export class AdminInterface extends AuthenticatedInterface {
this.user.user.isSuperuser ||
// TODO: somehow add `access_admin_interface` to the API schema
this.user.user.systemPermissions.includes("access_admin_interface");
if (!canAccessAdmin && this.user.user.pk > 0) {
window.location.assign("/if/user/");
}
@ -125,10 +182,14 @@ export class AdminInterface extends AuthenticatedInterface {
render(): TemplateResult {
const sidebarClasses = {
"pf-c-page__sidebar": true,
"pf-m-light": this.activeTheme === UiThemeEnum.Light,
"pf-m-expanded": this.sidebarOpen,
"pf-m-collapsed": !this.sidebarOpen,
};
const drawerOpen = this.notificationDrawerOpen || this.apiDrawerOpen;
const drawerClasses = {
"pf-m-expanded": drawerOpen,
"pf-m-collapsed": !drawerOpen,
@ -136,11 +197,18 @@ export class AdminInterface extends AuthenticatedInterface {
return html` <ak-locale-context>
<div class="pf-c-page">
<ak-enterprise-status interface="admin"></ak-enterprise-status>
<ak-version-banner></ak-version-banner>
<ak-admin-sidebar
class="pf-c-page__sidebar ${classMap(sidebarClasses)}"
></ak-admin-sidebar>
<ak-page-navbar>
<ak-version-banner></ak-version-banner>
<ak-enterprise-status interface="admin"></ak-enterprise-status>
</ak-page-navbar>
<ak-sidebar class="${classMap(sidebarClasses)}">
${renderSidebarItems(AdminSidebarEntries)}
${this.licenseSummary?.status !== LicenseSummaryStatusEnum.Unlicensed
? renderSidebarItems(AdminSidebarEnterpriseEntries)
: nothing}
</ak-sidebar>
<div class="pf-c-page__drawer">
<div class="pf-c-drawer ${classMap(drawerClasses)}">
<div class="pf-c-drawer__main">

View File

@ -94,10 +94,13 @@ export class AdminOverviewPage extends AdminOverviewBase {
}
render(): TemplateResult {
const name = this.user?.user.name ?? this.user?.user.username;
const username = this.user?.user.name || this.user?.user.username;
return html`<ak-page-header description=${msg("General system status")} ?hasIcon=${false}>
<span slot="header"> ${msg(str`Welcome, ${name || ""}.`)} </span>
return html` <ak-page-header
header=${msg(str`Welcome, ${username || ""}.`)}
description=${msg("General system status")}
?hasIcon=${false}
>
</ak-page-header>
<section class="pf-c-page__main-section">
<div class="pf-l-grid pf-m-gutter">

View File

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

View File

@ -83,13 +83,10 @@ export class AdminSettingsPage extends AKElement {
}
render() {
if (!this.settings) {
return nothing;
}
if (!this.settings) return nothing;
return html`
<ak-page-header icon="fa fa-cog" header="" description="">
<span slot="header"> ${msg("System settings")} </span>
</ak-page-header>
<ak-page-header icon="fa fa-cog" header="${msg("System settings")}"> </ak-page-header>
<section class="pf-c-page__main-section pf-m-no-padding-mobile pf-l-grid pf-m-gutter">
<div class="pf-c-card">
<div class="pf-c-card__body">

View File

@ -1,7 +1,6 @@
import "@goauthentik/admin/applications/ProviderSelectModal";
import { iconHelperText } from "@goauthentik/admin/helperText";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-file-input";
import "@goauthentik/components/ak-radio-input";
import "@goauthentik/components/ak-switch-input";
@ -194,7 +193,7 @@ export class ApplicationForm extends WithCapabilitiesConfig(ModelForm<Applicatio
></ak-text-input>
<ak-switch-input
name="openInNewTab"
?checked=${first(this.instance?.openInNewTab, false)}
?checked=${this.instance?.openInNewTab ?? false}
label=${msg("Open in new tab")}
help=${msg(
"If checked, the launch URL will open in a new browser tab or window from the user's application library.",
@ -221,7 +220,7 @@ export class ApplicationForm extends WithCapabilitiesConfig(ModelForm<Applicatio
: html` <ak-text-input
label=${msg("Icon")}
name="metaIcon"
value=${first(this.instance?.metaIcon, "")}
value=${this.instance?.metaIcon ?? ""}
help=${iconHelperText}
>
</ak-text-input>`}

View File

@ -1,5 +1,4 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/CodeMirror";
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/forms/HorizontalFormElement";
@ -60,7 +59,7 @@ export class ApplicationEntitlementForm extends ModelForm<ApplicationEntitlement
return html` <ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">
<input
type="text"
value="${first(this.instance?.name, "")}"
value="${this.instance?.name ?? ""}"
class="pf-c-form-control"
required
/>
@ -72,7 +71,7 @@ export class ApplicationEntitlementForm extends ModelForm<ApplicationEntitlement
>
<ak-codemirror
mode=${CodeMirrorMode.YAML}
value="${YAML.stringify(first(this.instance?.attributes, {}))}"
value="${YAML.stringify(this.instance?.attributes ?? {})}"
>
</ak-codemirror>
<p class="pf-c-form__helper-text">

View File

@ -1,7 +1,6 @@
import { policyOptions } from "@goauthentik/admin/applications/PolicyOptions.js";
import { ApplicationWizardStep } from "@goauthentik/admin/applications/wizard/ApplicationWizardStep.js";
import "@goauthentik/admin/applications/wizard/ak-wizard-title.js";
import { isSlug } from "@goauthentik/common/utils.js";
import { camelToSnake } from "@goauthentik/common/utils.js";
import "@goauthentik/components/ak-radio-input";
import "@goauthentik/components/ak-slug-input";
@ -11,6 +10,7 @@ import { type NavigableButton, type WizardButton } from "@goauthentik/components
import { type KeyUnknown } from "@goauthentik/elements/forms/Form";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { isSlug } from "@goauthentik/elements/router/utils.js";
import { msg } from "@lit/localize";
import { html } from "lit";

View File

@ -1,6 +1,5 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { docLink } from "@goauthentik/common/global";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-toggle-group";
import "@goauthentik/elements/CodeMirror";
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";
@ -80,7 +79,7 @@ export class BlueprintForm extends ModelForm<BlueprintInstance, string> {
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.enabled, true)}
?checked=${this.instance?.enabled ?? true}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
@ -184,7 +183,7 @@ export class BlueprintForm extends ModelForm<BlueprintInstance, string> {
<ak-form-element-horizontal label=${msg("Context")} name="context">
<ak-codemirror
mode=${CodeMirrorMode.YAML}
value="${YAML.stringify(first(this.instance?.context, {}))}"
value="${YAML.stringify(this.instance?.context ?? {})}"
>
</ak-codemirror>
<p class="pf-c-form__helper-text">

View File

@ -1,14 +1,13 @@
import "@goauthentik/admin/common/ak-crypto-certificate-search";
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import { DefaultBrand } from "@goauthentik/common/ui/config";
import "@goauthentik/elements/CodeMirror";
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
import "@goauthentik/elements/forms/SearchSelect";
import { DefaultBrand } from "@goauthentik/elements/sidebar/SidebarBrand";
import YAML from "yaml";
import { msg } from "@lit/localize";
@ -54,7 +53,7 @@ export class BrandForm extends ModelForm<Brand, string> {
return html` <ak-form-element-horizontal label=${msg("Domain")} required name="domain">
<input
type="text"
value="${first(this.instance?.domain, window.location.host)}"
value="${this.instance?.domain ?? window.location.host}"
class="pf-c-form-control pf-m-monospace"
autocomplete="off"
spellcheck="false"
@ -72,7 +71,7 @@ export class BrandForm extends ModelForm<Brand, string> {
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?._default, false)}
?checked=${this.instance?._default ?? false}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
@ -92,10 +91,7 @@ export class BrandForm extends ModelForm<Brand, string> {
<ak-form-element-horizontal label=${msg("Title")} required name="brandingTitle">
<input
type="text"
value="${first(
this.instance?.brandingTitle,
DefaultBrand.brandingTitle,
)}"
value="${this.instance?.brandingTitle ?? DefaultBrand.brandingTitle}"
class="pf-c-form-control"
required
/>
@ -106,7 +102,7 @@ export class BrandForm extends ModelForm<Brand, string> {
<ak-form-element-horizontal label=${msg("Logo")} required name="brandingLogo">
<input
type="text"
value="${first(this.instance?.brandingLogo, DefaultBrand.brandingLogo)}"
value="${this.instance?.brandingLogo ?? DefaultBrand.brandingLogo}"
class="pf-c-form-control pf-m-monospace"
autocomplete="off"
spellcheck="false"
@ -123,10 +119,8 @@ export class BrandForm extends ModelForm<Brand, string> {
>
<input
type="text"
value="${first(
this.instance?.brandingFavicon,
DefaultBrand.brandingFavicon,
)}"
value="${this.instance?.brandingFavicon ??
DefaultBrand.brandingFavicon}"
class="pf-c-form-control pf-m-monospace"
autocomplete="off"
spellcheck="false"
@ -143,10 +137,8 @@ export class BrandForm extends ModelForm<Brand, string> {
>
<input
type="text"
value="${first(
this.instance?.brandingDefaultFlowBackground,
"/static/dist/assets/images/flow_background.jpg",
)}"
value="${this.instance?.brandingDefaultFlowBackground ??
"/static/dist/assets/images/flow_background.jpg"}"
class="pf-c-form-control pf-m-monospace"
autocomplete="off"
spellcheck="false"
@ -165,10 +157,8 @@ export class BrandForm extends ModelForm<Brand, string> {
>
<ak-codemirror
mode=${CodeMirrorMode.CSS}
value="${first(
this.instance?.brandingCustomCss,
DefaultBrand.brandingCustomCss,
)}"
value="${this.instance?.brandingCustomCss ??
DefaultBrand.brandingCustomCss}"
>
</ak-codemirror>
<p class="pf-c-form__helper-text">
@ -317,7 +307,7 @@ export class BrandForm extends ModelForm<Brand, string> {
<ak-form-element-horizontal label=${msg("Attributes")} name="attributes">
<ak-codemirror
mode=${CodeMirrorMode.YAML}
value="${YAML.stringify(first(this.instance?.attributes, {}))}"
value="${YAML.stringify(this.instance?.attributes ?? {})}"
>
</ak-codemirror>
<p class="pf-c-form__helper-text">

View File

@ -1,5 +1,4 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
import "@goauthentik/elements/forms/Radio";
@ -185,7 +184,7 @@ export class TransportForm extends ModelForm<NotificationTransport, string> {
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.sendOnce, false)}
?checked=${this.instance?.sendOnce ?? false}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">

View File

@ -1,7 +1,6 @@
import { DesignationToLabel, LayoutToLabel } from "@goauthentik/admin/flows/utils";
import { AuthenticationEnum } from "@goauthentik/api/dist/models/AuthenticationEnum";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import {
CapabilitiesEnum,
WithCapabilitiesConfig,
@ -227,7 +226,7 @@ export class FlowForm extends WithCapabilitiesConfig(ModelForm<Flow, string>) {
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.compatibilityMode, false)}
?checked=${this.instance?.compatibilityMode ?? false}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
@ -407,7 +406,7 @@ export class FlowForm extends WithCapabilitiesConfig(ModelForm<Flow, string>) {
>
<input
type="text"
value="${first(this.instance?.background, "")}"
value="${this.instance?.background ?? ""}"
class="pf-c-form-control"
/>
<p class="pf-c-form__helper-text">

View File

@ -1,5 +1,5 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first, groupBy } from "@goauthentik/common/utils";
import { groupBy } from "@goauthentik/common/utils";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
import "@goauthentik/elements/forms/Radio";
@ -123,7 +123,7 @@ export class StageBindingForm extends ModelForm<FlowStageBinding, string> {
<ak-form-element-horizontal label=${msg("Order")} ?required=${true} name="order">
<input
type="number"
value="${first(this.instance?.order, this.defaultOrder)}"
value="${this.instance?.order ?? this.defaultOrder}"
class="pf-c-form-control"
required
/>
@ -133,7 +133,7 @@ export class StageBindingForm extends ModelForm<FlowStageBinding, string> {
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.evaluateOnPlan, false)}
?checked=${this.instance?.evaluateOnPlan ?? false}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
@ -151,7 +151,7 @@ export class StageBindingForm extends ModelForm<FlowStageBinding, string> {
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.reEvaluatePolicies, true)}
?checked=${this.instance?.reEvaluatePolicies ?? true}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">

View File

@ -1,6 +1,5 @@
import "@goauthentik/admin/groups/MemberSelectModal";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/CodeMirror";
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-provider";
@ -77,7 +76,7 @@ export class GroupForm extends ModelForm<Group, string> {
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.isSuperuser, false)}
?checked=${this.instance?.isSuperuser ?? false}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
@ -150,7 +149,7 @@ export class GroupForm extends ModelForm<Group, string> {
>
<ak-codemirror
mode=${CodeMirrorMode.YAML}
value="${YAML.stringify(first(this.instance?.attributes, {}))}"
value="${YAML.stringify(this.instance?.attributes ?? {})}"
>
</ak-codemirror>
<p class="pf-c-form__helper-text">

View File

@ -1,6 +1,5 @@
import "@goauthentik/admin/common/ak-crypto-certificate-search";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
import "@goauthentik/elements/forms/SearchSelect";
@ -53,7 +52,7 @@ export class ServiceConnectionDockerForm extends ModelForm<DockerServiceConnecti
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.local, false)}
?checked=${this.instance?.local ?? false}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">

View File

@ -1,5 +1,4 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/CodeMirror";
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/forms/HorizontalFormElement";
@ -57,7 +56,7 @@ export class ServiceConnectionKubernetesForm extends ModelForm<
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.local, false)}
?checked=${this.instance?.local ?? false}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
@ -75,7 +74,7 @@ export class ServiceConnectionKubernetesForm extends ModelForm<
<ak-form-element-horizontal label=${msg("Kubeconfig")} name="kubeconfig">
<ak-codemirror
mode=${CodeMirrorMode.YAML}
value="${YAML.stringify(first(this.instance?.kubeconfig, {}))}"
value="${YAML.stringify(this.instance?.kubeconfig ?? {})}"
>
</ak-codemirror>
<p class="pf-c-form__helper-text">
@ -87,7 +86,7 @@ export class ServiceConnectionKubernetesForm extends ModelForm<
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.verifySsl, true)}
?checked=${this.instance?.verifySsl ?? true}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">

View File

@ -3,7 +3,7 @@ import {
PolicyBindingCheckTargetToLabel,
} from "@goauthentik/admin/policies/utils";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first, groupBy } from "@goauthentik/common/utils";
import { groupBy } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-toggle-group";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
@ -274,7 +274,7 @@ export class PolicyBindingForm extends ModelForm<PolicyBinding, string> {
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.enabled, true)}
?checked=${this.instance?.enabled ?? true}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
@ -289,7 +289,7 @@ export class PolicyBindingForm extends ModelForm<PolicyBinding, string> {
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.negate, false)}
?checked=${this.instance?.negate ?? false}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
@ -305,7 +305,7 @@ export class PolicyBindingForm extends ModelForm<PolicyBinding, string> {
<ak-form-element-horizontal label=${msg("Order")} ?required=${true} name="order">
<input
type="number"
value="${first(this.instance?.order, this.defaultOrder)}"
value="${this.instance?.order ?? this.defaultOrder}"
class="pf-c-form-control"
required
/>
@ -313,7 +313,7 @@ export class PolicyBindingForm extends ModelForm<PolicyBinding, string> {
<ak-form-element-horizontal label=${msg("Timeout")} ?required=${true} name="timeout">
<input
type="number"
value="${first(this.instance?.timeout, 30)}"
value="${this.instance?.timeout ?? 30}"
class="pf-c-form-control"
required
/>

View File

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

View File

@ -1,6 +1,5 @@
import { BasePolicyForm } from "@goauthentik/admin/policies/BasePolicyForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
@ -51,7 +50,7 @@ export class DummyPolicyForm extends BasePolicyForm<DummyPolicy> {
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.executionLogging, false)}
?checked=${this.instance?.executionLogging ?? false}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
@ -74,7 +73,7 @@ export class DummyPolicyForm extends BasePolicyForm<DummyPolicy> {
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.result, false)}
?checked=${this.instance?.result ?? false}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
@ -91,7 +90,7 @@ export class DummyPolicyForm extends BasePolicyForm<DummyPolicy> {
>
<input
type="number"
value="${first(this.instance?.waitMin, 1)}"
value="${this.instance?.waitMin ?? 1}"
class="pf-c-form-control"
required
/>
@ -108,7 +107,7 @@ export class DummyPolicyForm extends BasePolicyForm<DummyPolicy> {
>
<input
type="number"
value="${first(this.instance?.waitMax, 5)}"
value="${this.instance?.waitMax ?? 5}"
class="pf-c-form-control"
required
/>

View File

@ -1,6 +1,5 @@
import { BasePolicyForm } from "@goauthentik/admin/policies/BasePolicyForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/SearchSelect";
@ -63,7 +62,7 @@ export class EventMatcherPolicyForm extends BasePolicyForm<EventMatcherPolicy> {
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.executionLogging, false)}
?checked=${this.instance?.executionLogging ?? false}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">

View File

@ -1,6 +1,5 @@
import { BasePolicyForm } from "@goauthentik/admin/policies/BasePolicyForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
@ -51,7 +50,7 @@ export class PasswordExpiryPolicyForm extends BasePolicyForm<PasswordExpiryPolic
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.executionLogging, false)}
?checked=${this.instance?.executionLogging ?? false}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
@ -86,7 +85,7 @@ export class PasswordExpiryPolicyForm extends BasePolicyForm<PasswordExpiryPolic
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.denyOnly, false)}
?checked=${this.instance?.denyOnly ?? false}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">

View File

@ -1,7 +1,6 @@
import { BasePolicyForm } from "@goauthentik/admin/policies/BasePolicyForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { docLink } from "@goauthentik/common/global";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/CodeMirror";
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/forms/FormGroup";
@ -54,7 +53,7 @@ export class ExpressionPolicyForm extends BasePolicyForm<ExpressionPolicy> {
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.executionLogging, false)}
?checked=${this.instance?.executionLogging ?? false}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">

View File

@ -1,6 +1,5 @@
import { BasePolicyForm } from "@goauthentik/admin/policies/BasePolicyForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/ak-dual-select";
import { DataProvision, DualSelectPair } from "@goauthentik/elements/ak-dual-select/types";
import "@goauthentik/elements/forms/FormGroup";
@ -112,7 +111,7 @@ export class GeoIPPolicyForm extends BasePolicyForm<GeoIPPolicy> {
<input
type="number"
min="1"
value="${first(this.instance?.historyMaxDistanceKm, 100)}"
value="${this.instance?.historyMaxDistanceKm ?? 100}"
class="pf-c-form-control"
/>
<p class="pf-c-form__helper-text">
@ -128,7 +127,7 @@ export class GeoIPPolicyForm extends BasePolicyForm<GeoIPPolicy> {
<input
type="number"
min="1"
value="${first(this.instance?.distanceToleranceKm, 50)}"
value="${this.instance?.distanceToleranceKm ?? 50}"
class="pf-c-form-control"
/>
<p class="pf-c-form__helper-text">
@ -142,7 +141,7 @@ export class GeoIPPolicyForm extends BasePolicyForm<GeoIPPolicy> {
<input
type="number"
min="1"
value="${first(this.instance?.historyLoginCount, 5)}"
value="${this.instance?.historyLoginCount ?? 5}"
class="pf-c-form-control"
/>
<p class="pf-c-form__helper-text">
@ -178,7 +177,7 @@ export class GeoIPPolicyForm extends BasePolicyForm<GeoIPPolicy> {
<input
type="number"
min="1"
value="${first(this.instance?.impossibleToleranceKm, 50)}"
value="${this.instance?.impossibleToleranceKm ?? 50}"
class="pf-c-form-control"
/>
<p class="pf-c-form__helper-text">

View File

@ -1,6 +1,5 @@
import { BasePolicyForm } from "@goauthentik/admin/policies/BasePolicyForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
@ -56,7 +55,7 @@ export class PasswordPolicyForm extends BasePolicyForm<PasswordPolicy> {
>
<input
type="number"
value="${first(this.instance?.lengthMin, 10)}"
value="${this.instance?.lengthMin ?? 10}"
class="pf-c-form-control"
required
/>
@ -68,7 +67,7 @@ export class PasswordPolicyForm extends BasePolicyForm<PasswordPolicy> {
>
<input
type="number"
value="${first(this.instance?.amountUppercase, 2)}"
value="${this.instance?.amountUppercase ?? 2}"
class="pf-c-form-control"
required
/>
@ -80,7 +79,7 @@ export class PasswordPolicyForm extends BasePolicyForm<PasswordPolicy> {
>
<input
type="number"
value="${first(this.instance?.amountLowercase, 2)}"
value="${this.instance?.amountLowercase ?? 2}"
class="pf-c-form-control"
required
/>
@ -92,7 +91,7 @@ export class PasswordPolicyForm extends BasePolicyForm<PasswordPolicy> {
>
<input
type="number"
value="${first(this.instance?.amountDigits, 2)}"
value="${this.instance?.amountDigits ?? 2}"
class="pf-c-form-control"
required
/>
@ -104,7 +103,7 @@ export class PasswordPolicyForm extends BasePolicyForm<PasswordPolicy> {
>
<input
type="number"
value="${first(this.instance?.amountSymbols, 2)}"
value="${this.instance?.amountSymbols ?? 2}"
class="pf-c-form-control"
required
/>
@ -154,7 +153,7 @@ export class PasswordPolicyForm extends BasePolicyForm<PasswordPolicy> {
>
<input
type="number"
value="${first(this.instance?.hibpAllowedCount, 0)}"
value="${this.instance?.hibpAllowedCount ?? 0}"
class="pf-c-form-control"
required
/>
@ -179,7 +178,7 @@ export class PasswordPolicyForm extends BasePolicyForm<PasswordPolicy> {
>
<input
type="number"
value="${first(this.instance?.zxcvbnScoreThreshold, 0)}"
value="${this.instance?.zxcvbnScoreThreshold ?? 0}"
class="pf-c-form-control"
required
/>
@ -236,7 +235,7 @@ export class PasswordPolicyForm extends BasePolicyForm<PasswordPolicy> {
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.executionLogging, false)}
?checked=${this.instance?.executionLogging ?? false}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
@ -272,7 +271,7 @@ export class PasswordPolicyForm extends BasePolicyForm<PasswordPolicy> {
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.checkStaticRules, true)}
?checked=${this.instance?.checkStaticRules ?? true}
@change=${(ev: Event) => {
const el = ev.target as HTMLInputElement;
this.showStatic = el.checked;
@ -291,7 +290,7 @@ export class PasswordPolicyForm extends BasePolicyForm<PasswordPolicy> {
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.checkHaveIBeenPwned, true)}
?checked=${this.instance?.checkHaveIBeenPwned ?? true}
@change=${(ev: Event) => {
const el = ev.target as HTMLInputElement;
this.showHIBP = el.checked;
@ -316,7 +315,7 @@ export class PasswordPolicyForm extends BasePolicyForm<PasswordPolicy> {
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.checkZxcvbn, true)}
?checked=${this.instance?.checkZxcvbn ?? true}
@change=${(ev: Event) => {
const el = ev.target as HTMLInputElement;
this.showZxcvbn = el.checked;

View File

@ -1,6 +1,5 @@
import { BasePolicyForm } from "@goauthentik/admin/policies/BasePolicyForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
@ -61,7 +60,7 @@ doesn't pass when either or both of the selected options are equal or above the
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.executionLogging, false)}
?checked=${this.instance?.executionLogging ?? false}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
@ -84,7 +83,7 @@ doesn't pass when either or both of the selected options are equal or above the
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.checkIp, true)}
?checked=${this.instance?.checkIp ?? true}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
@ -99,7 +98,7 @@ doesn't pass when either or both of the selected options are equal or above the
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.checkUsername, false)}
?checked=${this.instance?.checkUsername ?? false}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">

View File

@ -1,6 +1,5 @@
import { BasePolicyForm } from "@goauthentik/admin/policies/BasePolicyForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
@ -51,7 +50,7 @@ export class UniquePasswordPolicyForm extends BasePolicyForm<UniquePasswordPolic
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.executionLogging, false)}
?checked=${this.instance?.executionLogging ?? false}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
@ -88,7 +87,7 @@ export class UniquePasswordPolicyForm extends BasePolicyForm<UniquePasswordPolic
>
<input
type="number"
value="${first(this.instance?.numHistoricalPasswords, 1)}"
value="${this.instance?.numHistoricalPasswords ?? 1}"
class="pf-c-form-control"
required
/>

View File

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

View File

@ -4,7 +4,6 @@ import {
propertyMappingsSelector,
} from "@goauthentik/admin/providers/google_workspace/GoogleWorkspaceProviderFormHelpers.js";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/CodeMirror";
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
@ -68,7 +67,7 @@ export class GoogleWorkspaceProviderFormPage extends BaseProviderForm<GoogleWork
>
<ak-codemirror
mode=${CodeMirrorMode.JavaScript}
.value="${first(this.instance?.credentials, {})}"
.value="${this.instance?.credentials ?? {}}"
></ak-codemirror>
<p class="pf-c-form__helper-text">
${msg("Google Cloud credentials file.")}
@ -81,7 +80,7 @@ export class GoogleWorkspaceProviderFormPage extends BaseProviderForm<GoogleWork
>
<input
type="email"
value="${first(this.instance?.delegatedSubject, "")}"
value="${this.instance?.delegatedSubject ?? ""}"
class="pf-c-form-control pf-m-monospace"
required
/>
@ -98,7 +97,7 @@ export class GoogleWorkspaceProviderFormPage extends BaseProviderForm<GoogleWork
>
<input
type="text"
value="${first(this.instance?.defaultGroupEmailDomain, "")}"
value="${this.instance?.defaultGroupEmailDomain ?? ""}"
class="pf-c-form-control pf-m-monospace"
required
/>
@ -166,7 +165,7 @@ export class GoogleWorkspaceProviderFormPage extends BaseProviderForm<GoogleWork
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.dryRun, false)}
?checked=${this.instance?.dryRun ?? false}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
@ -191,7 +190,7 @@ export class GoogleWorkspaceProviderFormPage extends BaseProviderForm<GoogleWork
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.excludeUsersServiceAccount, true)}
?checked=${this.instance?.excludeUsersServiceAccount ?? true}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">

View File

@ -4,7 +4,6 @@ import {
propertyMappingsSelector,
} from "@goauthentik/admin/providers/microsoft_entra/MicrosoftEntraProviderFormHelpers.js";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-provider.js";
import "@goauthentik/elements/forms/FormGroup";
@ -66,7 +65,7 @@ export class MicrosoftEntraProviderFormPage extends BaseProviderForm<MicrosoftEn
>
<input
type="text"
value="${first(this.instance?.clientId, "")}"
value="${this.instance?.clientId ?? ""}"
class="pf-c-form-control pf-m-monospace"
required
/>
@ -81,7 +80,7 @@ export class MicrosoftEntraProviderFormPage extends BaseProviderForm<MicrosoftEn
>
<input
type="text"
value="${first(this.instance?.clientSecret, "")}"
value="${this.instance?.clientSecret ?? ""}"
class="pf-c-form-control pf-m-monospace"
required
/>
@ -96,7 +95,7 @@ export class MicrosoftEntraProviderFormPage extends BaseProviderForm<MicrosoftEn
>
<input
type="text"
value="${first(this.instance?.tenantId, "")}"
value="${this.instance?.tenantId ?? ""}"
class="pf-c-form-control pf-m-monospace"
required
/>
@ -155,7 +154,7 @@ export class MicrosoftEntraProviderFormPage extends BaseProviderForm<MicrosoftEn
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.dryRun, false)}
?checked=${this.instance?.dryRun ?? false}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
@ -180,7 +179,7 @@ export class MicrosoftEntraProviderFormPage extends BaseProviderForm<MicrosoftEn
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.excludeUsersServiceAccount, true)}
?checked=${this.instance?.excludeUsersServiceAccount ?? true}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">

View File

@ -4,7 +4,7 @@ import {
IRedirectURIInput,
akOAuthRedirectURIInput,
} from "@goauthentik/admin/providers/oauth2/OAuth2ProviderRedirectURI";
import { ascii_letters, digits, first, randomString } from "@goauthentik/common/utils";
import { ascii_letters, digits, randomString } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-radio-input";
import "@goauthentik/components/ak-text-input";
import "@goauthentik/components/ak-textarea-input";
@ -161,7 +161,7 @@ export function renderForm(
<ak-text-input
name="clientId"
label=${msg("Client ID")}
value="${first(provider?.clientId, randomString(40, ascii_letters + digits))}"
value="${provider?.clientId ?? randomString(40, ascii_letters + digits)}"
required
inputHint="code"
>
@ -169,10 +169,7 @@ export function renderForm(
<ak-text-input
name="clientSecret"
label=${msg("Client Secret")}
value="${first(
provider?.clientSecret,
randomString(128, ascii_letters + digits),
)}"
value="${provider?.clientSecret ?? randomString(128, ascii_letters + digits)}"
inputHint="code"
?hidden=${!showClientSecret}
>
@ -257,7 +254,7 @@ export function renderForm(
label=${msg("Access code validity")}
inputHint="code"
required
value="${first(provider?.accessCodeValidity, "minutes=1")}"
value="${provider?.accessCodeValidity ?? "minutes=1"}"
.bighelp=${html`<p class="pf-c-form__helper-text">
${msg("Configure how long access codes are valid for.")}
</p>
@ -267,7 +264,7 @@ export function renderForm(
<ak-text-input
name="accessTokenValidity"
label=${msg("Access Token validity")}
value="${first(provider?.accessTokenValidity, "minutes=5")}"
value="${provider?.accessTokenValidity ?? "minutes=5"}"
inputHint="code"
required
.bighelp=${html` <p class="pf-c-form__helper-text">
@ -280,7 +277,7 @@ export function renderForm(
<ak-text-input
name="refreshTokenValidity"
label=${msg("Refresh Token validity")}
value="${first(provider?.refreshTokenValidity, "days=30")}"
value="${provider?.refreshTokenValidity ?? "days=30"}"
inputHint="code"
?required=${true}
.bighelp=${html` <p class="pf-c-form__helper-text">
@ -317,7 +314,7 @@ export function renderForm(
<ak-switch-input
name="includeClaimsInIdToken"
label=${msg("Include claims in id_token")}
?checked=${first(provider?.includeClaimsInIdToken, true)}
?checked=${provider?.includeClaimsInIdToken ?? true}
help=${msg(
"Include User claims from scopes in the id_token, for applications that don't access the userinfo endpoint.",
)}

View File

@ -3,7 +3,6 @@ import "@goauthentik/admin/providers/proxy/ProxyProviderForm";
import "@goauthentik/admin/rbac/ObjectPermissionsPage";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_REFRESH } from "@goauthentik/common/constants";
import { convertToSlug } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-status-label";
import "@goauthentik/components/events/ObjectChangelog";
import MDCaddyStandalone from "@goauthentik/docs/add-secure-apps/providers/proxy/_caddy_standalone.md";
@ -22,6 +21,7 @@ import type { Replacer } from "@goauthentik/elements/ak-mdx";
import "@goauthentik/elements/buttons/ModalButton";
import "@goauthentik/elements/buttons/SpinnerButton";
import { getURLParam } from "@goauthentik/elements/router/RouteMatch";
import { formatSlug } from "@goauthentik/elements/router/utils.js";
import { msg } from "@lit/localize";
import { CSSResult, PropertyValues, TemplateResult, css, html } from "lit";
@ -183,7 +183,7 @@ export class ProxyProviderViewPage extends AKElement {
return html`<ak-tabs pageIdentifier="proxy-setup">
${servers.map((server) => {
return html`<section
slot="page-${convertToSlug(server.label)}"
slot="page-${formatSlug(server.label)}"
data-tab-title="${server.label}"
class="pf-c-page__main-section pf-m-no-padding-mobile ak-markdown-section"
>

View File

@ -1,5 +1,4 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-radio-input";
import "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
@ -99,7 +98,7 @@ export class EndpointForm extends ModelForm<Endpoint, string> {
>
<input
type="number"
value="${first(this.instance?.maximumConnections, 1)}"
value="${this.instance?.maximumConnections ?? 1}"
class="pf-c-form-control"
required
/>
@ -123,7 +122,7 @@ export class EndpointForm extends ModelForm<Endpoint, string> {
<ak-form-element-horizontal label=${msg("Settings")} name="settings">
<ak-codemirror
mode="yaml"
value="${YAML.stringify(first(this.instance?.settings, {}))}"
value="${YAML.stringify(this.instance?.settings ?? {})}"
>
</ak-codemirror>
<p class="pf-c-form__helper-text">${msg("Connection settings.")}</p>

View File

@ -1,7 +1,6 @@
import "@goauthentik/admin/common/ak-crypto-certificate-search";
import "@goauthentik/admin/common/ak-flow-search/ak-branded-flow-search";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
import "@goauthentik/elements/forms/FormGroup";
@ -82,7 +81,7 @@ export class RACProviderFormPage extends ModelForm<RACProvider, number> {
>
<input
type="text"
value="${first(this.instance?.connectionExpiry, "hours=8")}"
value="${this.instance?.connectionExpiry ?? "hours=8"}"
class="pf-c-form-control pf-m-monospace"
autocomplete="off"
spellcheck="false"
@ -100,7 +99,7 @@ export class RACProviderFormPage extends ModelForm<RACProvider, number> {
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.deleteTokenOnDisconnect, false)}
?checked=${this.instance?.deleteTokenOnDisconnect ?? false}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
@ -135,7 +134,7 @@ export class RACProviderFormPage extends ModelForm<RACProvider, number> {
<ak-form-element-horizontal label=${msg("Settings")} name="settings">
<ak-codemirror
mode="yaml"
value="${YAML.stringify(first(this.instance?.settings, {}))}"
value="${YAML.stringify(this.instance?.settings ?? {})}"
>
</ak-codemirror>
<p class="pf-c-form__helper-text">${msg("Connection settings.")}</p>

View File

@ -1,6 +1,6 @@
import "@goauthentik/admin/common/ak-flow-search/ak-branded-flow-search";
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
import { ascii_letters, digits, first, randomString } from "@goauthentik/common/utils";
import { ascii_letters, digits, randomString } from "@goauthentik/common/utils";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/SearchSelect";
@ -78,17 +78,14 @@ export function renderForm(
name="sharedSecret"
label=${msg("Shared secret")}
.errorMessages=${errors?.sharedSecret ?? []}
value=${first(
provider?.sharedSecret,
randomString(128, ascii_letters + digits),
)}
value=${provider?.sharedSecret ?? randomString(128, ascii_letters + digits)}
required
inputHint="code"
></ak-text-input>
<ak-text-input
name="clientNetworks"
label=${msg("Client Networks")}
value=${first(provider?.clientNetworks, "0.0.0.0/0, ::/0")}
value=${provider?.clientNetworks ?? "0.0.0.0/0, ::/0"}
.errorMessages=${errors?.clientNetworks ?? []}
required
help=${clientNetworksHelp}

View File

@ -1,5 +1,4 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
@ -37,7 +36,7 @@ export function renderForm(provider?: Partial<SCIMProvider>, errors: ValidationE
<ak-text-input
name="url"
label=${msg("URL")}
value="${first(provider?.url, "")}"
value="${provider?.url ?? ""}"
.errorMessages=${errors?.url ?? []}
required
help=${msg("SCIM base url, usually ends in /v2.")}
@ -96,7 +95,7 @@ export function renderForm(provider?: Partial<SCIMProvider>, errors: ValidationE
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(provider?.dryRun, false)}
?checked=${provider?.dryRun ?? false}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
@ -119,7 +118,7 @@ export function renderForm(provider?: Partial<SCIMProvider>, errors: ValidationE
<ak-switch-input
name="excludeUsersServiceAccount"
label=${msg("Exclude service accounts")}
?checked=${first(provider?.excludeUsersServiceAccount, true)}
?checked=${provider?.excludeUsersServiceAccount ?? true}
>
</ak-switch-input>

View File

@ -5,7 +5,6 @@ import {
oauth2ProvidersSelector,
} from "@goauthentik/admin/providers/oauth2/OAuth2ProvidersProvider";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-text-input";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-provider.js";
@ -81,7 +80,7 @@ export class SSFProviderFormPage extends BaseProviderForm<SSFProvider> {
>
<input
type="text"
value="${first(provider?.eventRetention, "days=30")}"
value="${provider?.eventRetention ?? "days=30"}"
class="pf-c-form-control"
required
/>

View File

@ -89,19 +89,24 @@ export class RoleObjectPermissionForm extends ModelForm<RoleAssignData, number>
>
</ak-search-select>
</ak-form-element-horizontal>
${this.modelPermissions?.results.map((perm) => {
return html` <ak-form-element-horizontal name="permissions.${perm.codename}">
<label class="pf-c-switch">
<input class="pf-c-switch__input" type="checkbox" />
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
<i class="fas fa-check" aria-hidden="true"></i>
${this.modelPermissions?.results
.filter((perm) => {
const [_app, model] = this.model?.split(".") || "";
return perm.codename !== `add_${model}`;
})
.map((perm) => {
return html` <ak-form-element-horizontal name="permissions.${perm.codename}">
<label class="pf-c-switch">
<input class="pf-c-switch__input" type="checkbox" />
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
<i class="fas fa-check" aria-hidden="true"></i>
</span>
</span>
</span>
<span class="pf-c-switch__label">${perm.name}</span>
</label>
</ak-form-element-horizontal>`;
})}
<span class="pf-c-switch__label">${perm.name}</span>
</label>
</ak-form-element-horizontal>`;
})}
</form>`;
}
}

View File

@ -45,7 +45,7 @@ export class RoleAssignedObjectPermissionTable extends Table<RoleAssignedObjectP
ordering: "codename",
});
modelPermissions.results = modelPermissions.results.filter((value) => {
return !value.codename.startsWith("add_");
return value.codename !== `add_${this.model?.split(".")[1]}`;
});
this.modelPermissions = modelPermissions;
return perms;

View File

@ -6,7 +6,6 @@ import {
UserMatchingModeToLabel,
} from "@goauthentik/admin/sources/oauth/utils";
import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-switch-input";
import "@goauthentik/components/ak-text-input";
import "@goauthentik/components/ak-textarea-input";
@ -97,12 +96,12 @@ export class KerberosSourceForm extends WithCapabilitiesConfig(BaseSourceForm<Ke
></ak-text-input>
<ak-switch-input
name="enabled"
?checked=${first(this.instance?.enabled, true)}
?checked=${this.instance?.enabled ?? true}
label=${msg("Enabled")}
></ak-switch-input>
<ak-switch-input
name="passwordLoginUpdateInternalPassword"
?checked=${first(this.instance?.passwordLoginUpdateInternalPassword, false)}
?checked=${this.instance?.passwordLoginUpdateInternalPassword ?? false}
label=${msg("Update internal password on login")}
help=${msg(
"When the user logs in to authentik using this source password backend, update their credentials in authentik.",
@ -110,12 +109,12 @@ export class KerberosSourceForm extends WithCapabilitiesConfig(BaseSourceForm<Ke
></ak-switch-input>
<ak-switch-input
name="syncUsers"
?checked=${first(this.instance?.syncUsers, true)}
?checked=${this.instance?.syncUsers ?? true}
label=${msg("Sync users")}
></ak-switch-input>
<ak-switch-input
name="syncUsersPassword"
?checked=${first(this.instance?.syncUsersPassword, true)}
?checked=${this.instance?.syncUsersPassword ?? true}
label=${msg("User password writeback")}
help=${msg(
"Enable this option to write password changes made in authentik back to Kerberos. Ignored if sync is disabled.",
@ -395,10 +394,8 @@ export class KerberosSourceForm extends WithCapabilitiesConfig(BaseSourceForm<Ke
<ak-text-input
name="userPathTemplate"
label=${msg("User path")}
value=${first(
this.instance?.userPathTemplate,
"goauthentik.io/sources/%(slug)s",
)}
value=${this.instance?.userPathTemplate ??
"goauthentik.io/sources/%(slug)s"}
help=${placeholderHelperText}
></ak-text-input>
</div>
@ -443,7 +440,7 @@ export class KerberosSourceForm extends WithCapabilitiesConfig(BaseSourceForm<Ke
: html`<ak-form-element-horizontal label=${msg("Icon")} name="icon">
<input
type="text"
value="${first(this.instance?.icon, "")}"
value="${this.instance?.icon ?? ""}"
class="pf-c-form-control"
/>
<p class="pf-c-form__helper-text">${iconHelperText}</p>

View File

@ -2,7 +2,6 @@ import "@goauthentik/admin/common/ak-crypto-certificate-search";
import { placeholderHelperText } from "@goauthentik/admin/helperText";
import { BaseSourceForm } from "@goauthentik/admin/sources/BaseSourceForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
@ -67,7 +66,7 @@ export class LDAPSourceForm extends BaseSourceForm<LDAPSource> {
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.enabled, true)}
?checked=${this.instance?.enabled ?? true}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
@ -82,7 +81,7 @@ export class LDAPSourceForm extends BaseSourceForm<LDAPSource> {
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.passwordLoginUpdateInternalPassword, false)}
?checked=${this.instance?.passwordLoginUpdateInternalPassword ?? false}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
@ -104,7 +103,7 @@ export class LDAPSourceForm extends BaseSourceForm<LDAPSource> {
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.syncUsers, true)}
?checked=${this.instance?.syncUsers ?? true}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
@ -119,7 +118,7 @@ export class LDAPSourceForm extends BaseSourceForm<LDAPSource> {
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.syncUsersPassword, true)}
?checked=${this.instance?.syncUsersPassword ?? true}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
@ -139,7 +138,7 @@ export class LDAPSourceForm extends BaseSourceForm<LDAPSource> {
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.syncGroups, true)}
?checked=${this.instance?.syncGroups ?? true}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
@ -173,7 +172,7 @@ export class LDAPSourceForm extends BaseSourceForm<LDAPSource> {
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.startTls, true)}
?checked=${this.instance?.startTls ?? true}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
@ -191,7 +190,7 @@ export class LDAPSourceForm extends BaseSourceForm<LDAPSource> {
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.sni, false)}
?checked=${this.instance?.sni ?? false}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
@ -335,10 +334,8 @@ export class LDAPSourceForm extends BaseSourceForm<LDAPSource> {
<ak-form-element-horizontal label=${msg("User path")} name="userPathTemplate">
<input
type="text"
value="${first(
this.instance?.userPathTemplate,
"goauthentik.io/sources/%(slug)s",
)}"
value="${this.instance?.userPathTemplate ??
"goauthentik.io/sources/%(slug)s"}"
class="pf-c-form-control"
/>
<p class="pf-c-form__helper-text">${placeholderHelperText}</p>
@ -421,7 +418,7 @@ export class LDAPSourceForm extends BaseSourceForm<LDAPSource> {
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.lookupGroupsFromUser, false)}
?checked=${this.instance?.lookupGroupsFromUser ?? false}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">

View File

@ -6,7 +6,6 @@ import {
UserMatchingModeToLabel,
} from "@goauthentik/admin/sources/oauth/utils";
import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-radio-input";
import "@goauthentik/elements/CodeMirror";
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";
@ -136,11 +135,9 @@ export class OAuthSourceForm extends WithCapabilitiesConfig(BaseSourceForm<OAuth
>
<input
type="text"
value="${first(
this.instance?.authorizationUrl,
this.providerType.authorizationUrl,
"",
)}"
value="${this.instance?.authorizationUrl ??
this.providerType.authorizationUrl ??
""}"
class="pf-c-form-control pf-m-monospace"
autocomplete="off"
spellcheck="false"
@ -152,11 +149,9 @@ export class OAuthSourceForm extends WithCapabilitiesConfig(BaseSourceForm<OAuth
<ak-form-element-horizontal label=${msg("Access token URL")} name="accessTokenUrl">
<input
type="url"
value="${first(
this.instance?.accessTokenUrl,
this.providerType.accessTokenUrl,
"",
)}"
value="${this.instance?.accessTokenUrl ??
this.providerType.accessTokenUrl ??
""}"
class="pf-c-form-control pf-m-monospace"
autocomplete="off"
spellcheck="false"
@ -168,11 +163,7 @@ export class OAuthSourceForm extends WithCapabilitiesConfig(BaseSourceForm<OAuth
<ak-form-element-horizontal label=${msg("Profile URL")} name="profileUrl">
<input
type="url"
value="${first(
this.instance?.profileUrl,
this.providerType.profileUrl,
"",
)}"
value="${this.instance?.profileUrl ?? this.providerType.profileUrl ?? ""}"
class="pf-c-form-control pf-m-monospace"
autocomplete="off"
spellcheck="false"
@ -188,7 +179,7 @@ export class OAuthSourceForm extends WithCapabilitiesConfig(BaseSourceForm<OAuth
>
<input
type="url"
value="${first(this.instance?.requestTokenUrl, "")}"
value="${this.instance?.requestTokenUrl ?? ""}"
class="pf-c-form-control pf-m-monospace"
autocomplete="off"
/>
@ -207,11 +198,9 @@ export class OAuthSourceForm extends WithCapabilitiesConfig(BaseSourceForm<OAuth
>
<input
type="url"
value="${first(
this.instance?.oidcWellKnownUrl,
this.providerType.oidcWellKnownUrl,
"",
)}"
value="${this.instance?.oidcWellKnownUrl ??
this.providerType.oidcWellKnownUrl ??
""}"
class="pf-c-form-control pf-m-monospace"
autocomplete="off"
spellcheck="false"
@ -231,11 +220,9 @@ export class OAuthSourceForm extends WithCapabilitiesConfig(BaseSourceForm<OAuth
>
<input
type="url"
value="${first(
this.instance?.oidcJwksUrl,
this.providerType.oidcJwksUrl,
"",
)}"
value="${this.instance?.oidcJwksUrl ??
this.providerType.oidcJwksUrl ??
""}"
class="pf-c-form-control pf-m-monospace"
autocomplete="off"
spellcheck="false"
@ -249,7 +236,7 @@ export class OAuthSourceForm extends WithCapabilitiesConfig(BaseSourceForm<OAuth
<ak-form-element-horizontal label=${msg("OIDC JWKS")} name="oidcJwks">
<ak-codemirror
mode=${CodeMirrorMode.JavaScript}
value="${JSON.stringify(first(this.instance?.oidcJwks, {}))}"
value="${JSON.stringify(this.instance?.oidcJwks ?? {})}"
>
</ak-codemirror>
<p class="pf-c-form__helper-text">${msg("Raw JWKS data.")}</p>
@ -296,7 +283,7 @@ export class OAuthSourceForm extends WithCapabilitiesConfig(BaseSourceForm<OAuth
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.enabled, true)}
?checked=${this.instance?.enabled ?? true}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
@ -381,10 +368,7 @@ export class OAuthSourceForm extends WithCapabilitiesConfig(BaseSourceForm<OAuth
<ak-form-element-horizontal label=${msg("User path")} name="userPathTemplate">
<input
type="text"
value="${first(
this.instance?.userPathTemplate,
"goauthentik.io/sources/%(slug)s",
)}"
value="${this.instance?.userPathTemplate ?? "goauthentik.io/sources/%(slug)s"}"
class="pf-c-form-control pf-m-monospace"
autocomplete="off"
spellcheck="false"
@ -432,7 +416,7 @@ export class OAuthSourceForm extends WithCapabilitiesConfig(BaseSourceForm<OAuth
: html`<ak-form-element-horizontal label=${msg("Icon")} name="icon">
<input
type="text"
value="${first(this.instance?.icon, "")}"
value="${this.instance?.icon ?? ""}"
class="pf-c-form-control pf-m-monospace"
autocomplete="off"
spellcheck="false"
@ -474,7 +458,7 @@ export class OAuthSourceForm extends WithCapabilitiesConfig(BaseSourceForm<OAuth
<ak-form-element-horizontal label=${msg("Scopes")} name="additionalScopes">
<input
type="text"
value="${first(this.instance?.additionalScopes, "")}"
value="${this.instance?.additionalScopes ?? ""}"
class="pf-c-form-control pf-m-monospace"
autocomplete="off"
spellcheck="false"

View File

@ -7,7 +7,7 @@ import {
} from "@goauthentik/admin/sources/oauth/utils";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { PlexAPIClient, PlexResource, popupCenterScreen } from "@goauthentik/common/helpers/plex";
import { ascii_letters, digits, first, randomString } from "@goauthentik/common/utils";
import { ascii_letters, digits, randomString } from "@goauthentik/common/utils";
import {
CapabilitiesEnum,
WithCapabilitiesConfig,
@ -137,7 +137,7 @@ export class PlexSourceForm extends WithCapabilitiesConfig(BaseSourceForm<PlexSo
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.allowFriends, true)}
?checked=${this.instance?.allowFriends ?? true}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
@ -198,7 +198,7 @@ export class PlexSourceForm extends WithCapabilitiesConfig(BaseSourceForm<PlexSo
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.enabled, true)}
?checked=${this.instance?.enabled ?? true}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
@ -283,10 +283,7 @@ export class PlexSourceForm extends WithCapabilitiesConfig(BaseSourceForm<PlexSo
<ak-form-element-horizontal label=${msg("User path")} name="userPathTemplate">
<input
type="text"
value="${first(
this.instance?.userPathTemplate,
"goauthentik.io/sources/%(slug)s",
)}"
value="${this.instance?.userPathTemplate ?? "goauthentik.io/sources/%(slug)s"}"
class="pf-c-form-control"
/>
<p class="pf-c-form__helper-text">${placeholderHelperText}</p>
@ -332,7 +329,7 @@ export class PlexSourceForm extends WithCapabilitiesConfig(BaseSourceForm<PlexSo
: html`<ak-form-element-horizontal label=${msg("Icon")} name="icon">
<input
type="text"
value="${first(this.instance?.icon, "")}"
value="${this.instance?.icon ?? ""}"
class="pf-c-form-control"
/>
<p class="pf-c-form__helper-text">${iconHelperText}</p>
@ -347,7 +344,7 @@ export class PlexSourceForm extends WithCapabilitiesConfig(BaseSourceForm<PlexSo
>
<input
type="text"
value="${first(this.instance?.clientId, "")}"
value="${this.instance?.clientId ?? ""}"
class="pf-c-form-control"
required
/>

View File

@ -7,7 +7,6 @@ import {
UserMatchingModeToLabel,
} from "@goauthentik/admin/sources/oauth/utils";
import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import {
CapabilitiesEnum,
WithCapabilitiesConfig,
@ -105,7 +104,7 @@ export class SAMLSourceForm extends WithCapabilitiesConfig(BaseSourceForm<SAMLSo
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.enabled, true)}
?checked=${this.instance?.enabled ?? true}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
@ -228,7 +227,7 @@ export class SAMLSourceForm extends WithCapabilitiesConfig(BaseSourceForm<SAMLSo
: html`<ak-form-element-horizontal label=${msg("Icon")} name="icon">
<input
type="text"
value="${first(this.instance?.icon, "")}"
value="${this.instance?.icon ?? ""}"
class="pf-c-form-control"
/>
<p class="pf-c-form__helper-text">${iconHelperText}</p>
@ -334,7 +333,7 @@ export class SAMLSourceForm extends WithCapabilitiesConfig(BaseSourceForm<SAMLSo
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.allowIdpInitiated, false)}
?checked=${this.instance?.allowIdpInitiated ?? false}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
@ -397,10 +396,8 @@ export class SAMLSourceForm extends WithCapabilitiesConfig(BaseSourceForm<SAMLSo
<ak-form-element-horizontal label=${msg("User path")} name="userPathTemplate">
<input
type="text"
value="${first(
this.instance?.userPathTemplate,
"goauthentik.io/sources/%(slug)s",
)}"
value="${this.instance?.userPathTemplate ??
"goauthentik.io/sources/%(slug)s"}"
class="pf-c-form-control"
/>
<p class="pf-c-form__helper-text">${placeholderHelperText}</p>

View File

@ -1,7 +1,6 @@
import { placeholderHelperText } from "@goauthentik/admin/helperText";
import { BaseSourceForm } from "@goauthentik/admin/sources/BaseSourceForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
@ -63,7 +62,7 @@ export class SCIMSourceForm extends BaseSourceForm<SCIMSource> {
<input
type="checkbox"
class="pf-c-check__input"
?checked=${first(this.instance?.enabled, true)}
?checked=${this.instance?.enabled ?? true}
/>
<label class="pf-c-check__label"> ${msg("Enabled")} </label>
</div>
@ -111,10 +110,8 @@ export class SCIMSourceForm extends BaseSourceForm<SCIMSource> {
<ak-form-element-horizontal label=${msg("User path")} name="userPathTemplate">
<input
type="text"
value="${first(
this.instance?.userPathTemplate,
"goauthentik.io/sources/%(slug)s",
)}"
value="${this.instance?.userPathTemplate ??
"goauthentik.io/sources/%(slug)s"}"
class="pf-c-form-control"
/>
<p class="pf-c-form__helper-text">${placeholderHelperText}</p>

View File

@ -1,7 +1,6 @@
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/SearchSelect";
@ -50,7 +49,7 @@ export class AuthenticatorDuoStageForm extends BaseStageForm<AuthenticatorDuoSta
<ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">
<input
type="text"
value="${first(this.instance?.name, "")}"
value="${this.instance?.name ?? ""}"
class="pf-c-form-control"
required
/>
@ -62,7 +61,7 @@ export class AuthenticatorDuoStageForm extends BaseStageForm<AuthenticatorDuoSta
>
<input
type="text"
value="${first(this.instance?.friendlyName, "")}"
value="${this.instance?.friendlyName ?? ""}"
class="pf-c-form-control"
/>
<p class="pf-c-form__helper-text">
@ -78,7 +77,7 @@ export class AuthenticatorDuoStageForm extends BaseStageForm<AuthenticatorDuoSta
>
<input
type="text"
value="${first(this.instance?.apiHostname, "")}"
value="${this.instance?.apiHostname ?? ""}"
class="pf-c-form-control pf-m-monospace"
autocomplete="off"
spellcheck="false"
@ -95,7 +94,7 @@ export class AuthenticatorDuoStageForm extends BaseStageForm<AuthenticatorDuoSta
>
<input
type="text"
value="${first(this.instance?.clientId, "")}"
value="${this.instance?.clientId ?? ""}"
class="pf-c-form-control"
required
/>
@ -132,7 +131,7 @@ export class AuthenticatorDuoStageForm extends BaseStageForm<AuthenticatorDuoSta
>
<input
type="text"
value="${first(this.instance?.adminIntegrationKey, "")}"
value="${this.instance?.adminIntegrationKey ?? ""}"
class="pf-c-form-control pf-m-monospace"
autocomplete="off"
spellcheck="false"

View File

@ -1,7 +1,6 @@
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/Radio";
@ -65,7 +64,7 @@ export class AuthenticatorEmailStageForm extends BaseStageForm<AuthenticatorEmai
<ak-form-element-horizontal label=${msg("SMTP Port")} ?required=${true} name="port">
<input
type="number"
value="${first(this.instance?.port, 25)}"
value="${this.instance?.port ?? 25}"
class="pf-c-form-control"
required
/>
@ -89,7 +88,7 @@ export class AuthenticatorEmailStageForm extends BaseStageForm<AuthenticatorEmai
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.useTls, true)}
?checked=${this.instance?.useTls ?? true}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
@ -104,7 +103,7 @@ export class AuthenticatorEmailStageForm extends BaseStageForm<AuthenticatorEmai
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.useSsl, false)}
?checked=${this.instance?.useSsl ?? false}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
@ -121,7 +120,7 @@ export class AuthenticatorEmailStageForm extends BaseStageForm<AuthenticatorEmai
>
<input
type="number"
value="${first(this.instance?.timeout, 30)}"
value="${this.instance?.timeout ?? 30}"
class="pf-c-form-control"
required
/>
@ -150,7 +149,7 @@ export class AuthenticatorEmailStageForm extends BaseStageForm<AuthenticatorEmai
<ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">
<input
type="text"
value="${first(this.instance?.name, "")}"
value="${this.instance?.name ?? ""}"
class="pf-c-form-control"
required
/>
@ -162,7 +161,7 @@ export class AuthenticatorEmailStageForm extends BaseStageForm<AuthenticatorEmai
>
<input
type="text"
value="${first(this.instance?.friendlyName, "")}"
value="${this.instance?.friendlyName ?? ""}"
class="pf-c-form-control"
/>
<p class="pf-c-form__helper-text">
@ -176,7 +175,7 @@ export class AuthenticatorEmailStageForm extends BaseStageForm<AuthenticatorEmai
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.useGlobalSettings, true)}
?checked=${this.instance?.useGlobalSettings ?? true}
@change=${(ev: Event) => {
const target = ev.target as HTMLInputElement;
this.showConnectionSettings = !target.checked;
@ -206,7 +205,7 @@ export class AuthenticatorEmailStageForm extends BaseStageForm<AuthenticatorEmai
>
<input
type="text"
value="${first(this.instance?.subject, "authentik Sign-in code")}"
value="${this.instance?.subject ?? "authentik Sign-in code"}"
class="pf-c-form-control"
required
/>
@ -221,7 +220,7 @@ export class AuthenticatorEmailStageForm extends BaseStageForm<AuthenticatorEmai
>
<input
type="text"
value="${first(this.instance?.tokenExpiry, "minutes=15")}"
value="${this.instance?.tokenExpiry ?? "minutes=15"}"
class="pf-c-form-control"
required
/>

View File

@ -1,6 +1,5 @@
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/CodeMirror";
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/forms/FormGroup";
@ -52,7 +51,7 @@ export class AuthenticatorEndpointGDTCStageForm extends BaseStageForm<Authentica
<ak-form-element-horizontal label=${msg("Name")} required name="name">
<input
type="text"
value="${first(this.instance?.name, "")}"
value="${this.instance?.name ?? ""}"
class="pf-c-form-control"
required
/>
@ -67,7 +66,7 @@ export class AuthenticatorEndpointGDTCStageForm extends BaseStageForm<Authentica
>
<ak-codemirror
mode=${CodeMirrorMode.JavaScript}
.value="${first(this.instance?.credentials, {})}"
.value="${this.instance?.credentials ?? {}}"
></ak-codemirror>
<p class="pf-c-form__helper-text">
${msg("Google Cloud credentials file.")}

View File

@ -1,7 +1,6 @@
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/Radio";
@ -66,7 +65,7 @@ export class AuthenticatorSMSStageForm extends BaseStageForm<AuthenticatorSMSSta
>
<input
type="text"
value="${first(this.instance?.accountSid, "")}"
value="${this.instance?.accountSid ?? ""}"
class="pf-c-form-control pf-m-monospace"
autocomplete="off"
spellcheck="false"
@ -83,7 +82,7 @@ export class AuthenticatorSMSStageForm extends BaseStageForm<AuthenticatorSMSSta
>
<input
type="text"
value="${first(this.instance?.auth, "")}"
value="${this.instance?.auth ?? ""}"
class="pf-c-form-control pf-m-monospace"
autocomplete="off"
spellcheck="false"
@ -129,7 +128,7 @@ export class AuthenticatorSMSStageForm extends BaseStageForm<AuthenticatorSMSSta
>
<input
type="text"
value="${first(this.instance?.accountSid, "")}"
value="${this.instance?.accountSid ?? ""}"
class="pf-c-form-control pf-m-monospace"
autocomplete="off"
spellcheck="false"
@ -146,7 +145,7 @@ export class AuthenticatorSMSStageForm extends BaseStageForm<AuthenticatorSMSSta
>
<input
type="text"
value="${first(this.instance?.auth, "")}"
value="${this.instance?.auth ?? ""}"
class="pf-c-form-control pf-m-monospace"
autocomplete="off"
spellcheck="false"
@ -164,7 +163,7 @@ export class AuthenticatorSMSStageForm extends BaseStageForm<AuthenticatorSMSSta
>
<input
type="text"
value="${first(this.instance?.authPassword, "")}"
value="${this.instance?.authPassword ?? ""}"
class="pf-c-form-control pf-m-monospace"
autocomplete="off"
spellcheck="false"
@ -215,7 +214,7 @@ export class AuthenticatorSMSStageForm extends BaseStageForm<AuthenticatorSMSSta
<ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">
<input
type="text"
value="${first(this.instance?.name, "")}"
value="${this.instance?.name ?? ""}"
class="pf-c-form-control"
required
/>
@ -227,7 +226,7 @@ export class AuthenticatorSMSStageForm extends BaseStageForm<AuthenticatorSMSSta
>
<input
type="text"
value="${first(this.instance?.friendlyName, "")}"
value="${this.instance?.friendlyName ?? ""}"
class="pf-c-form-control"
/>
<p class="pf-c-form__helper-text">
@ -272,7 +271,7 @@ export class AuthenticatorSMSStageForm extends BaseStageForm<AuthenticatorSMSSta
>
<input
type="text"
value="${first(this.instance?.fromNumber, "")}"
value="${this.instance?.fromNumber ?? ""}"
class="pf-c-form-control pf-m-monospace"
autocomplete="off"
spellcheck="false"
@ -290,7 +289,7 @@ export class AuthenticatorSMSStageForm extends BaseStageForm<AuthenticatorSMSSta
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.verifyOnly, false)}
?checked=${this.instance?.verifyOnly ?? false}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">

View File

@ -1,7 +1,6 @@
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/SearchSelect";
@ -48,7 +47,7 @@ export class AuthenticatorStaticStageForm extends BaseStageForm<AuthenticatorSta
<ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">
<input
type="text"
value="${first(this.instance?.name, "")}"
value="${this.instance?.name ?? ""}"
class="pf-c-form-control"
required
/>
@ -60,7 +59,7 @@ export class AuthenticatorStaticStageForm extends BaseStageForm<AuthenticatorSta
>
<input
type="text"
value="${first(this.instance?.friendlyName, "")}"
value="${this.instance?.friendlyName ?? ""}"
class="pf-c-form-control"
/>
<p class="pf-c-form__helper-text">
@ -79,7 +78,7 @@ export class AuthenticatorStaticStageForm extends BaseStageForm<AuthenticatorSta
>
<input
type="text"
value="${first(this.instance?.tokenCount, 6)}"
value="${this.instance?.tokenCount ?? 6}"
class="pf-c-form-control"
required
/>
@ -96,7 +95,7 @@ export class AuthenticatorStaticStageForm extends BaseStageForm<AuthenticatorSta
>
<input
type="text"
value="${first(this.instance?.tokenLength, 12)}"
value="${this.instance?.tokenLength ?? 12}"
class="pf-c-form-control"
required
/>

View File

@ -1,7 +1,6 @@
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/SearchSelect";
@ -50,7 +49,7 @@ export class AuthenticatorTOTPStageForm extends BaseStageForm<AuthenticatorTOTPS
<ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">
<input
type="text"
value="${first(this.instance?.name, "")}"
value="${this.instance?.name ?? ""}"
class="pf-c-form-control"
required
/>
@ -62,7 +61,7 @@ export class AuthenticatorTOTPStageForm extends BaseStageForm<AuthenticatorTOTPS
>
<input
type="text"
value="${first(this.instance?.friendlyName, "")}"
value="${this.instance?.friendlyName ?? ""}"
class="pf-c-form-control"
/>
<p class="pf-c-form__helper-text">

View File

@ -2,7 +2,6 @@ import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
import { deviceTypeRestrictionPair } from "@goauthentik/admin/stages/authenticator_webauthn/utils";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-provider";
import { DataProvision } from "@goauthentik/elements/ak-dual-select/types";
import "@goauthentik/elements/forms/HorizontalFormElement";
@ -58,7 +57,7 @@ export class AuthenticatorWebAuthnStageForm extends BaseStageForm<AuthenticatorW
<ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">
<input
type="text"
value="${first(this.instance?.name, "")}"
value="${this.instance?.name ?? ""}"
class="pf-c-form-control"
required
/>
@ -70,7 +69,7 @@ export class AuthenticatorWebAuthnStageForm extends BaseStageForm<AuthenticatorW
>
<input
type="text"
value="${first(this.instance?.friendlyName, "")}"
value="${this.instance?.friendlyName ?? ""}"
class="pf-c-form-control"
/>
<p class="pf-c-form__helper-text">

View File

@ -1,6 +1,5 @@
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-number-input";
import "@goauthentik/components/ak-switch-input";
import "@goauthentik/elements/forms/FormGroup";
@ -118,7 +117,7 @@ export class CaptchaStageForm extends BaseStageForm<CaptchaStage> {
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.errorOnInvalidScore, true)}
?checked=${this.instance?.errorOnInvalidScore ?? true}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">

View File

@ -1,6 +1,5 @@
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { msg } from "@lit/localize";
@ -50,7 +49,7 @@ export class DummyStageForm extends BaseStageForm<DummyStage> {
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.throwError, false)}
?checked=${this.instance?.throwError ?? false}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">

View File

@ -1,6 +1,5 @@
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/utils/TimeDeltaHelp";
@ -62,7 +61,7 @@ export class EmailStageForm extends BaseStageForm<EmailStage> {
<ak-form-element-horizontal label=${msg("SMTP Port")} ?required=${true} name="port">
<input
type="number"
value="${first(this.instance?.port, 25)}"
value="${this.instance?.port ?? 25}"
class="pf-c-form-control"
required
/>
@ -86,7 +85,7 @@ export class EmailStageForm extends BaseStageForm<EmailStage> {
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.useTls, true)}
?checked=${this.instance?.useTls ?? true}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
@ -101,7 +100,7 @@ export class EmailStageForm extends BaseStageForm<EmailStage> {
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.useSsl, false)}
?checked=${this.instance?.useSsl ?? false}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
@ -118,7 +117,7 @@ export class EmailStageForm extends BaseStageForm<EmailStage> {
>
<input
type="number"
value="${first(this.instance?.timeout, 30)}"
value="${this.instance?.timeout ?? 30}"
class="pf-c-form-control"
required
/>
@ -161,7 +160,7 @@ export class EmailStageForm extends BaseStageForm<EmailStage> {
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.activateUserOnSuccess, true)}
?checked=${this.instance?.activateUserOnSuccess ?? true}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
@ -183,7 +182,7 @@ export class EmailStageForm extends BaseStageForm<EmailStage> {
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.useGlobalSettings, true)}
?checked=${this.instance?.useGlobalSettings ?? true}
@change=${(ev: Event) => {
const target = ev.target as HTMLInputElement;
this.showConnectionSettings = !target.checked;
@ -209,7 +208,7 @@ export class EmailStageForm extends BaseStageForm<EmailStage> {
>
<input
type="text"
value="${first(this.instance?.tokenExpiry, "minutes=30")}"
value="${this.instance?.tokenExpiry ?? "minutes=30"}"
class="pf-c-form-control"
required
/>
@ -225,7 +224,7 @@ export class EmailStageForm extends BaseStageForm<EmailStage> {
>
<input
type="text"
value="${first(this.instance?.subject, "authentik")}"
value="${this.instance?.subject ?? "authentik"}"
class="pf-c-form-control"
required
/>

View File

@ -1,7 +1,7 @@
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first, groupBy } from "@goauthentik/common/utils";
import { groupBy } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-switch-input.js";
import "@goauthentik/elements/ak-checkbox-group/ak-checkbox-group.js";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
@ -162,7 +162,7 @@ export class IdentificationStageForm extends BaseStageForm<IdentificationStage>
<ak-switch-input
name="caseInsensitiveMatching"
label=${msg("Case insensitive matching")}
?checked=${first(this.instance?.caseInsensitiveMatching, true)}
?checked=${this.instance?.caseInsensitiveMatching ?? true}
help=${msg(
"When enabled, user fields are matched regardless of their casing.",
)}
@ -170,7 +170,7 @@ export class IdentificationStageForm extends BaseStageForm<IdentificationStage>
<ak-switch-input
name="pretendUserExists"
label=${msg("Pretend user exists")}
?checked=${first(this.instance?.pretendUserExists, true)}
?checked=${this.instance?.pretendUserExists ?? true}
help=${msg(
"When enabled, the stage will always accept the given user identifier and continue.",
)}
@ -178,7 +178,7 @@ export class IdentificationStageForm extends BaseStageForm<IdentificationStage>
<ak-switch-input
name="showMatchedUser"
label=${msg("Show matched user")}
?checked=${first(this.instance?.showMatchedUser, true)}
?checked=${this.instance?.showMatchedUser ?? true}
help=${msg(
"When a valid username/email has been entered, and this option is enabled, the user's username and avatar will be shown. Otherwise, the text that the user entered will be shown.",
)}
@ -218,7 +218,7 @@ export class IdentificationStageForm extends BaseStageForm<IdentificationStage>
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.showSourceLabels, false)}
?checked=${this.instance?.showSourceLabels ?? false}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">

View File

@ -1,7 +1,6 @@
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { dateTimeLocal } from "@goauthentik/common/temporal";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/CodeMirror";
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/forms/HorizontalFormElement";
@ -63,7 +62,7 @@ export class InvitationForm extends ModelForm<Invitation, string> {
data-type="datetime-local"
class="pf-c-form-control"
required
value="${dateTimeLocal(first(this.instance?.expires, new Date()))}"
value="${dateTimeLocal(this.instance?.expires ?? new Date())}"
/>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Flow")} name="flow">
@ -80,7 +79,7 @@ export class InvitationForm extends ModelForm<Invitation, string> {
<ak-form-element-horizontal label=${msg("Custom attributes")} name="fixedData">
<ak-codemirror
mode=${CodeMirrorMode.YAML}
value="${YAML.stringify(first(this.instance?.fixedData, {}))}"
value="${YAML.stringify(this.instance?.fixedData ?? {})}"
>
</ak-codemirror>
<p class="pf-c-form__helper-text">
@ -94,7 +93,7 @@ export class InvitationForm extends ModelForm<Invitation, string> {
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.singleUse, true)}
?checked=${this.instance?.singleUse ?? true}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">

View File

@ -1,6 +1,5 @@
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
@ -51,10 +50,7 @@ export class InvitationStageForm extends BaseStageForm<InvitationStage> {
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(
this.instance?.continueFlowWithoutInvitation,
false,
)}
?checked=${this.instance?.continueFlowWithoutInvitation ?? false}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">

View File

@ -1,6 +1,5 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { parseAPIResponseError, pluckErrorDetail } from "@goauthentik/common/errors/network";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/CodeMirror";
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/forms/HorizontalFormElement";
@ -269,7 +268,7 @@ export class PromptForm extends ModelForm<Prompt, string> {
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.required, false)}
?checked=${this.instance?.required ?? false}
@change=${() => {
this._shouldRefresh = true;
}}
@ -287,7 +286,7 @@ export class PromptForm extends ModelForm<Prompt, string> {
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.placeholderExpression, false)}
?checked=${this.instance?.placeholderExpression ?? false}
@change=${() => {
this._shouldRefresh = true;
}}
@ -330,7 +329,7 @@ export class PromptForm extends ModelForm<Prompt, string> {
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.initialValueExpression, false)}
?checked=${this.instance?.initialValueExpression ?? false}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
@ -376,7 +375,7 @@ export class PromptForm extends ModelForm<Prompt, string> {
<ak-form-element-horizontal label=${msg("Order")} ?required=${true} name="order">
<input
type="number"
value="${first(this.instance?.order, 0)}"
value="${this.instance?.order ?? 0}"
class="pf-c-form-control"
required
/>

View File

@ -1,6 +1,5 @@
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/Alert";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
@ -38,7 +37,7 @@ export class UserLoginStageForm extends BaseStageForm<UserLoginStage> {
<ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">
<input
type="text"
value="${first(this.instance?.name, "")}"
value="${this.instance?.name ?? ""}"
class="pf-c-form-control"
required
/>
@ -53,7 +52,7 @@ export class UserLoginStageForm extends BaseStageForm<UserLoginStage> {
>
<input
type="text"
value="${first(this.instance?.sessionDuration, "seconds=0")}"
value="${this.instance?.sessionDuration ?? "seconds=0"}"
class="pf-c-form-control pf-m-monospace"
autocomplete="off"
spellcheck="false"
@ -84,7 +83,7 @@ export class UserLoginStageForm extends BaseStageForm<UserLoginStage> {
>
<input
type="text"
value="${first(this.instance?.rememberMeOffset, "seconds=0")}"
value="${this.instance?.rememberMeOffset ?? "seconds=0"}"
class="pf-c-form-control pf-m-monospace"
autocomplete="off"
spellcheck="false"
@ -170,7 +169,7 @@ export class UserLoginStageForm extends BaseStageForm<UserLoginStage> {
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.terminateOtherSessions, false)}
?checked=${this.instance?.terminateOtherSessions ?? false}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">

View File

@ -1,7 +1,6 @@
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
import { UserCreationModeEnum } from "@goauthentik/api/dist/models/UserCreationModeEnum";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/Radio";
@ -95,7 +94,7 @@ export class UserWriteStageForm extends BaseStageForm<UserWriteStage> {
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.createUsersAsInactive, true)}
?checked=${this.instance?.createUsersAsInactive ?? true}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
@ -149,7 +148,7 @@ export class UserWriteStageForm extends BaseStageForm<UserWriteStage> {
>
<input
type="text"
value="${first(this.instance?.userPathTemplate, "")}"
value="${this.instance?.userPathTemplate ?? ""}"
class="pf-c-form-control pf-m-monospace"
autocomplete="off"
spellcheck="false"

View File

@ -1,6 +1,5 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { dateTimeLocal } from "@goauthentik/common/temporal";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
@ -50,7 +49,7 @@ export class TokenForm extends ModelForm<Token, string> {
<input
type="datetime-local"
data-type="datetime-local"
value="${dateTimeLocal(first(this.instance?.expires, new Date()))}"
value="${dateTimeLocal(this.instance?.expires ?? new Date())}"
class="pf-c-form-control"
/>
</ak-form-element-horizontal>`;
@ -64,7 +63,7 @@ export class TokenForm extends ModelForm<Token, string> {
>
<input
type="text"
value="${first(this.instance?.identifier, "")}"
value="${this.instance?.identifier ?? ""}"
class="pf-c-form-control pf-m-monospace"
autocomplete="off"
spellcheck="false"
@ -123,7 +122,7 @@ export class TokenForm extends ModelForm<Token, string> {
<ak-form-element-horizontal label=${msg("Description")} name="description">
<input
type="text"
value="${first(this.instance?.description, "")}"
value="${this.instance?.description ?? ""}"
class="pf-c-form-control"
/>
</ak-form-element-horizontal>
@ -132,7 +131,7 @@ export class TokenForm extends ModelForm<Token, string> {
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.expiring, true)}
?checked=${this.instance?.expiring ?? true}
@change=${(ev: Event) => {
const el = ev.target as HTMLInputElement;
this.showExpiry = el.checked;

View File

@ -1,6 +1,5 @@
import "@goauthentik/admin/users/GroupSelectModal";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/CodeMirror";
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/forms/HorizontalFormElement";
@ -159,7 +158,7 @@ export class UserForm extends ModelForm<User, number> {
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.isActive, true)}
?checked=${this.instance?.isActive ?? true}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
@ -177,7 +176,7 @@ export class UserForm extends ModelForm<User, number> {
<ak-form-element-horizontal label=${msg("Path")} ?required=${true} name="path">
<input
type="text"
value="${first(this.instance?.path, this.defaultPath)}"
value="${this.instance?.path ?? this.defaultPath}"
class="pf-c-form-control"
required
/>
@ -190,7 +189,7 @@ export class UserForm extends ModelForm<User, number> {
<ak-codemirror
mode=${CodeMirrorMode.YAML}
value="${YAML.stringify(
first(this.instance?.attributes, UserForm.defaultUserAttributes),
this.instance?.attributes ?? UserForm.defaultUserAttributes,
)}"
>
</ak-codemirror>

View File

@ -1,26 +1,110 @@
import type { Config as DOMPurifyConfig } from "dompurify";
import DOMPurify from "dompurify";
import { trustedTypes } from "trusted-types";
import { render } from "@lit-labs/ssr";
import { collectResult } from "@lit-labs/ssr/lib/render-result.js";
import { TemplateResult, html } from "lit";
import { render } from "lit";
import { unsafeHTML } from "lit/directives/unsafe-html.js";
import { until } from "lit/directives/until.js";
/**
* Trusted types policy that escapes HTML content in place.
*
* @see {@linkcode SanitizedTrustPolicy} to strip HTML content.
*
* @returns {TrustedHTML} All HTML content, escaped.
*/
export const EscapeTrustPolicy = trustedTypes.createPolicy("authentik-escape", {
createHTML: (untrustedHTML: string) => {
return DOMPurify.sanitize(untrustedHTML, {
RETURN_TRUSTED_TYPE: false,
});
},
});
/**
* Trusted types policy, stripping all HTML content.
*
* @returns {TrustedHTML} Text content only, all HTML tags stripped.
*/
export const SanitizedTrustPolicy = trustedTypes.createPolicy("authentik-sanitize", {
createHTML: (untrustedHTML: string) => {
return DOMPurify.sanitize(untrustedHTML, {
RETURN_TRUSTED_TYPE: false,
ALLOWED_TAGS: ["#text"],
});
},
});
/**
* Trusted types policy, allowing a minimal set of _safe_ HTML tags supplied by
* a trusted source, such as the brand API.
*/
export const BrandedHTMLPolicy = trustedTypes.createPolicy("authentik-restrict", {
createHTML: (untrustedHTML: string) => {
return DOMPurify.sanitize(untrustedHTML, {
RETURN_TRUSTED_TYPE: false,
FORBID_TAGS: [
"script",
"style",
"iframe",
"link",
"object",
"embed",
"applet",
"meta",
"base",
"form",
"input",
"textarea",
"select",
"button",
],
FORBID_ATTR: [
"onerror",
"onclick",
"onload",
"onmouseover",
"onmouseout",
"onmouseup",
"onmousedown",
"onfocus",
"onblur",
"onsubmit",
],
});
},
});
export type AuthentikTrustPolicy =
| typeof EscapeTrustPolicy
| typeof SanitizedTrustPolicy
| typeof BrandedHTMLPolicy;
/**
* Sanitize an untrusted HTML string using a trusted types policy.
*/
export function sanitizeHTML(trustPolicy: AuthentikTrustPolicy, untrustedHTML: string) {
return unsafeHTML(trustPolicy.createHTML(untrustedHTML).toString());
}
/**
* DOMPurify configuration for strict sanitization.
*
* This configuration only allows text nodes and disallows all HTML tags.
*/
export const DOM_PURIFY_STRICT = {
ALLOWED_TAGS: ["#text"],
} as const satisfies DOMPurifyConfig;
export async function renderStatic(input: TemplateResult): Promise<string> {
return await collectResult(render(input));
}
/**
* Render untrusted HTML to a string without escaping it.
*
* @returns {string} The rendered HTML string.
*/
export function renderStaticHTMLUnsafe(untrustedHTML: unknown): string {
const container = document.createElement("html");
render(untrustedHTML, container);
export function purify(input: TemplateResult): TemplateResult {
return html`${until(
(async () => {
const rendered = await renderStatic(input);
const purified = DOMPurify.sanitize(rendered);
return html`${unsafeHTML(purified)}`;
})(),
)}`;
const result = container.innerHTML;
return result;
}

View File

@ -1,6 +1,7 @@
import { config } from "@goauthentik/common/api/config";
import { VERSION } from "@goauthentik/common/constants";
import { me } from "@goauthentik/common/users";
import { readInterfaceRouteParam } from "@goauthentik/elements/router/utils";
import {
ErrorEvent,
EventHint,
@ -68,7 +69,7 @@ export async function configureSentry(canDoPpi = false): Promise<Config> {
});
setTag(TAG_SENTRY_CAPABILITIES, cfg.capabilities.join(","));
if (window.location.pathname.includes("if/")) {
setTag(TAG_SENTRY_COMPONENT, `web/${currentInterface()}`);
setTag(TAG_SENTRY_COMPONENT, `web/${readInterfaceRouteParam()}`);
}
if (cfg.capabilities.includes(CapabilitiesEnum.CanDebug)) {
const Spotlight = await import("@spotlightjs/spotlight");
@ -86,13 +87,3 @@ export async function configureSentry(canDoPpi = false): Promise<Config> {
}
return cfg;
}
// Get the interface name from URL
export function currentInterface(): string {
const pathMatches = window.location.pathname.match(/.+if\/(\w+)\//);
let currentInterface = "unknown";
if (pathMatches && pathMatches.length >= 2) {
currentInterface = pathMatches[1];
}
return currentInterface.toLowerCase();
}

View File

@ -17,6 +17,13 @@
/* Minimum width after which the sidebar becomes automatic */
--ak-sidebar--minimum-auto-width: 80rem;
/**
* The height of the navbar and branded sidebar.
* @todo This shouldn't be necessary. The sidebar can instead use a grid layout
* ensuring they share the same height.
*/
--ak-navbar--height: 7rem;
}
@supports selector(::-webkit-scrollbar) {

View File

@ -0,0 +1,223 @@
/**
* @file Stylesheet utilities.
*/
import { CSSResult, CSSResultOrNative, ReactiveElement, css } from "lit";
/**
* Elements containing adoptable stylesheets.
*/
export type StyleSheetParent = Pick<DocumentOrShadowRoot, "adoptedStyleSheets">;
/**
* Type-predicate to determine if a given object has adoptable stylesheets.
*/
export function isAdoptableStyleSheetParent(input: unknown): input is StyleSheetParent {
// Sanity check - Does the input have the right shape?
if (!input || typeof input !== "object") return false;
if (!("adoptedStyleSheets" in input) || !input.adoptedStyleSheets) return false;
if (typeof input.adoptedStyleSheets !== "object") return false;
// We avoid `Array.isArray` because the adopted stylesheets property
// is defined as a proxied array.
// All we care about is that it's shaped like an array.
if (!("length" in input.adoptedStyleSheets)) return false;
if (typeof input.adoptedStyleSheets.length !== "number") return false;
// Finally is the array mutable?
return "push" in input.adoptedStyleSheets;
}
/**
* Assert that the given input can adopt stylesheets.
*/
export function assertAdoptableStyleSheetParent<T>(
input: T,
): asserts input is T & StyleSheetParent {
if (isAdoptableStyleSheetParent(input)) return;
console.debug("Given input missing `adoptedStyleSheets`", input);
throw new TypeError("Assertion failed: `adoptedStyleSheets` missing in given input");
}
export function resolveStyleSheetParent<T extends HTMLElement | DocumentFragment | Document>(
renderRoot: T,
) {
const styleRoot = "ShadyDOM" in window ? document : renderRoot;
assertAdoptableStyleSheetParent(styleRoot);
return styleRoot;
}
export type StyleSheetInit = string | CSSResult | CSSStyleSheet;
/**
* Given a source of CSS, create a `CSSStyleSheet`.
*
* @throw {@linkcode TypeError} if the input cannot be converted to a `CSSStyleSheet`
*
* @remarks
*
* Storybook's `build` does not currently have a coherent way of importing
* CSS-as-text into CSSStyleSheet.
*
* It works well when Storybook is running in `dev`, but in `build` it fails.
* Storied components will have to map their textual CSS imports.
*/
export function createStyleSheet(input: string): CSSResult {
const inputTemplate = [input] as unknown as TemplateStringsArray;
const result = css(inputTemplate, []);
return result;
}
/**
* Given a source of CSS, create a `CSSStyleSheet`.
*
* @see {@linkcode createStyleSheet}
*/
export function normalizeCSSSource(css: string): CSSStyleSheet;
export function normalizeCSSSource(styleSheet: CSSStyleSheet): CSSStyleSheet;
export function normalizeCSSSource(cssResult: CSSResult): CSSResult;
export function normalizeCSSSource(input: StyleSheetInit): CSSResultOrNative;
export function normalizeCSSSource(input: StyleSheetInit): CSSResultOrNative {
if (typeof input === "string") return createStyleSheet(input);
return input;
}
/**
* Create a `CSSStyleSheet` from the given input.
*/
export function createStyleSheetUnsafe(input: StyleSheetInit): CSSStyleSheet {
const result = normalizeCSSSource(input);
if (result instanceof CSSStyleSheet) return result;
if (!result.styleSheet) {
console.debug(
"authentik/common/stylesheets: CSSResult missing styleSheet, returning empty",
{ result, input },
);
throw new TypeError("Expected a CSSStyleSheet");
}
return result.styleSheet;
}
/**
* Append stylesheet(s) to the given roots.
*
* @see {@linkcode removeStyleSheet} to remove a stylesheet from a given roots.
*/
export function appendStyleSheet(
styleParent: StyleSheetParent,
...insertions: CSSStyleSheet[]
): void {
insertions = Array.isArray(insertions) ? insertions : [insertions];
for (const styleSheetInsertion of insertions) {
if (styleParent.adoptedStyleSheets.includes(styleSheetInsertion)) return;
styleParent.adoptedStyleSheets = [...styleParent.adoptedStyleSheets, styleSheetInsertion];
}
}
/**
* Remove a stylesheet from the given roots, matching by referential equality.
*
* @see {@linkcode appendStyleSheet} to append a stylesheet to a given roots.
*/
export function removeStyleSheet(
styleParent: StyleSheetParent,
...removals: CSSStyleSheet[]
): void {
const nextAdoptedStyleSheets = styleParent.adoptedStyleSheets.filter(
(styleSheet) => !removals.includes(styleSheet),
);
if (nextAdoptedStyleSheets.length === styleParent.adoptedStyleSheets.length) return;
styleParent.adoptedStyleSheets = nextAdoptedStyleSheets;
}
/**
* Serialize a stylesheet to a string.
*
* This is useful for debugging or inspecting the contents of a stylesheet.
*/
export function serializeStyleSheet(stylesheet: CSSStyleSheet): string {
return Array.from(stylesheet.cssRules || [], (rule) => rule.cssText || "").join("\n");
}
/**
* Inspect the adopted stylesheets of a given style parent, serializing them to strings.
*/
export function inspectStyleSheets(styleParent: StyleSheetParent): string[] {
return styleParent.adoptedStyleSheets.map((styleSheet) => serializeStyleSheet(styleSheet));
}
interface InspectedStyleSheetEntry {
tagName: string;
element: ReactiveElement;
styles: string[];
children?: InspectedStyleSheetEntry[];
}
/**
* Recursively inspect the adopted stylesheets of a given style parent, serializing them to strings.
*/
export function inspectStyleSheetTree(element: ReactiveElement): InspectedStyleSheetEntry {
const styleParent = resolveStyleSheetParent(element.renderRoot);
const styles = inspectStyleSheets(styleParent);
const tagName = element.tagName.toLowerCase();
const treewalker = document.createTreeWalker(element.renderRoot, NodeFilter.SHOW_ELEMENT, {
acceptNode(node) {
if (node instanceof ReactiveElement) {
return NodeFilter.FILTER_ACCEPT;
}
return NodeFilter.FILTER_SKIP;
},
});
const children: InspectedStyleSheetEntry[] = [];
let currentNode: Node | null = treewalker.nextNode();
while (currentNode) {
const childElement = currentNode as ReactiveElement;
if (!isAdoptableStyleSheetParent(childElement.renderRoot)) {
currentNode = treewalker.nextNode();
continue;
}
const childStyles = inspectStyleSheets(childElement.renderRoot);
children.push({
tagName: childElement.tagName.toLowerCase(),
element: childElement,
styles: childStyles,
});
currentNode = treewalker.nextNode();
}
return {
tagName,
element,
styles,
children,
};
}
if (process.env.NODE_ENV === "development") {
Object.assign(window, {
inspectStyleSheetTree,
serializeStyleSheet,
inspectStyleSheets,
});
}

200
web/src/common/theme.ts Normal file
View File

@ -0,0 +1,200 @@
/**
* @file Theme utilities.
*/
import { UIConfig } from "@goauthentik/common/ui/config";
import { Config, CurrentBrand, UiThemeEnum } from "@goauthentik/api";
//#region Scheme Types
/**
* Valid CSS color scheme values.
*
* @link {@link https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme | MDN}
*
* @category CSS
*/
export type CSSColorSchemeValue = "dark" | "light" | "auto";
/**
* A CSS color scheme value that can be preferred by the user, i.e. not `"auto"`.
*
* @category CSS
*/
export type ResolvedCSSColorSchemeValue = Exclude<CSSColorSchemeValue, "auto">;
//#endregion
//#region UI Theme Types
/**
* A UI color scheme value that can be preferred by the user.
*
* i.e. not an lack of preference or unknown value.
*
* @category CSS
*/
export type ResolvedUITheme = typeof UiThemeEnum.Light | typeof UiThemeEnum.Dark;
/**
* A mapping of theme values to their respective inversion.
*
* @category CSS
*/
export const UIThemeInversion = {
dark: "light",
light: "dark",
} as const satisfies Record<ResolvedUITheme, ResolvedUITheme>;
/**
* Either a valid CSS color scheme value, or a theme preference.
*/
export type UIThemeHint = CSSColorSchemeValue | UiThemeEnum;
//#endregion
//#region Scheme Functions
/**
* Creates an event target for the given color scheme.
*
* @param colorScheme The color scheme to target.
* @returns A {@linkcode MediaQueryList} that can be used to listen for changes to the color scheme.
*
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/MediaQueryList | MDN}
*
* @category CSS
*/
export function createColorSchemeTarget(colorScheme: ResolvedCSSColorSchemeValue): MediaQueryList {
return window.matchMedia(`(prefers-color-scheme: ${colorScheme})`);
}
/**
* Formats the given input into a valid CSS color scheme value.
*
* If the input is not provided, it defaults to "auto".
*
* @category CSS
*/
export function formatColorScheme(theme: ResolvedUITheme): ResolvedCSSColorSchemeValue;
export function formatColorScheme(
colorScheme: ResolvedCSSColorSchemeValue,
): ResolvedCSSColorSchemeValue;
export function formatColorScheme(hint?: UIThemeHint): CSSColorSchemeValue;
export function formatColorScheme(hint?: UIThemeHint): CSSColorSchemeValue {
if (!hint) return "auto";
switch (hint) {
case "dark":
case UiThemeEnum.Dark:
return "dark";
case "light":
case UiThemeEnum.Light:
return "light";
case "auto":
case UiThemeEnum.Automatic:
return "auto";
default:
console.warn(`Unknown color scheme hint: ${hint}. Defaulting to "auto".`);
return "auto";
}
}
//#endregion
//#region Theme Functions
/**
* Resolve the current UI theme based on the user's preference or the provided color scheme.
*
* @param hint The color scheme hint to use.
*
* @category CSS
*/
export function resolveUITheme(
hint?: UIThemeHint,
defaultUITheme: ResolvedUITheme = UiThemeEnum.Light,
): ResolvedUITheme {
const colorScheme = formatColorScheme(hint);
if (colorScheme !== "auto") return colorScheme;
// Given that we don't know the user's preference,
// we can determine the theme based on whether the default theme is
// currently being overridden.
const colorSchemeInversion = formatColorScheme(UIThemeInversion[defaultUITheme]);
const mediaQueryList = createColorSchemeTarget(colorSchemeInversion);
return mediaQueryList.matches ? colorSchemeInversion : defaultUITheme;
}
/**
* Effect listener invoked when the color scheme changes.
*/
export type UIThemeListener = (currentUITheme: ResolvedUITheme) => void;
/**
* Create an effect that runs
*
* @returns A cleanup function that removes the effect.
*/
export function createUIThemeEffect(
effect: UIThemeListener,
listenerOptions?: AddEventListenerOptions,
): () => void {
const colorSchemeTarget = resolveUITheme();
const invertedColorSchemeTarget = UIThemeInversion[colorSchemeTarget];
let previousUITheme: ResolvedUITheme | undefined;
// First, wrap the effect to ensure we can abort it.
const changeListener = (event: MediaQueryListEvent) => {
if (listenerOptions?.signal?.aborted) return;
const currentUITheme = event.matches ? colorSchemeTarget : invertedColorSchemeTarget;
if (previousUITheme === currentUITheme) return;
previousUITheme = currentUITheme;
effect(currentUITheme);
};
const mediaQueryList = createColorSchemeTarget(colorSchemeTarget);
// Trigger the effect immediately.
effect(colorSchemeTarget);
// Listen for changes to the color scheme...
mediaQueryList.addEventListener("change", changeListener, listenerOptions);
// Finally, allow the caller to remove the effect.
const cleanup = () => {
mediaQueryList.removeEventListener("change", changeListener);
};
return cleanup;
}
//#endregion
//#region Theme Element
/**
* An element that can be themed.
*/
export interface ThemedElement extends HTMLElement {
brand?: CurrentBrand;
uiConfig?: UIConfig;
config?: Config;
activeTheme: ResolvedUITheme;
}
export function rootInterface<T extends ThemedElement = ThemedElement>(): T | null {
const element = document.body.querySelector<T>("[data-ak-interface-root]");
return element;
}
//#endregion

View File

@ -1,7 +1,19 @@
import { currentInterface } from "@goauthentik/common/sentry";
import { me } from "@goauthentik/common/users";
import { isUserRoute } from "@goauthentik/elements/router/utils";
import { UiThemeEnum, UserSelf } from "@goauthentik/api";
import { CurrentBrand } from "@goauthentik/api";
export const DefaultBrand = {
brandingLogo: "/static/dist/assets/icons/icon_left_brand.svg",
brandingFavicon: "/static/dist/assets/icons/icon.png",
brandingTitle: "authentik",
brandingCustomCss: "",
uiFooterLinks: [],
uiTheme: UiThemeEnum.Automatic,
matchedDomain: "",
defaultLocale: "",
} as const satisfies CurrentBrand;
export enum UserDisplay {
username = "username",
@ -77,9 +89,7 @@ export class DefaultUIConfig implements UIConfig {
};
constructor() {
if (currentInterface() === "user") {
this.enabledFeatures.apiDrawer = false;
}
this.enabledFeatures.apiDrawer = !isUserRoute();
}
}

View File

@ -1,5 +1,3 @@
import { SentryIgnoredError } from "@goauthentik/common/sentry";
import { CSSResult, css } from "lit";
export function getCookie(name: string): string {
@ -18,19 +16,6 @@ export function getCookie(name: string): string {
return cookieValue;
}
export function convertToSlug(text: string): string {
return text
.toLowerCase()
.replace(/ /g, "-")
.replace(/[^\w-]+/g, "");
}
export function isSlug(text: string): boolean {
const lowered = text.toLowerCase();
const forbidden = /([^\w-]|\s)/.test(lowered);
return lowered === text && !forbidden;
}
/**
* Truncate a string based on maximum word count
*/
@ -74,16 +59,6 @@ export function groupBy<T>(objects: T[], callback: (obj: T) => string): Array<[s
return Array.from(m).sort();
}
export function first<T>(...args: Array<T | undefined | null>): T {
for (let index = 0; index < args.length; index++) {
const element = args[index];
if (element !== undefined && element !== null) {
return element;
}
}
throw new SentryIgnoredError(`No compatible arg given: ${args}`);
}
// Taken from python's string module
export const ascii_lowercase = "abcdefghijklmnopqrstuvwxyz";
export const ascii_uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
@ -96,7 +71,9 @@ export const punctuation = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";
export function randomString(len: number, charset: string): string {
const chars = [];
const array = new Uint8Array(len);
self.crypto.getRandomValues(array);
crypto.getRandomValues(array);
for (let index = 0; index < len; index++) {
chars.push(charset[Math.floor(charset.length * (array[index] / Math.pow(2, 8)))]);
}

View File

@ -1,5 +1,3 @@
import { first } from "@goauthentik/common/utils";
import { TemplateResult, html, nothing } from "lit";
import { classMap } from "lit/directives/class-map.js";
import { map } from "lit/directives/map.js";
@ -80,10 +78,10 @@ export function renderDescriptionList(
) {
const checkedTerms = alignTermType(terms);
const classes = classMap({
"pf-m-horizontal": first(config.horizontal, false),
"pf-m-compact": first(config.compact, false),
"pf-m-2-col-on-lg": first(config.twocolumn, false),
"pf-m-3-col-on-lg": first(config.threecolumn, false),
"pf-m-horizontal": config.horizontal ?? false,
"pf-m-compact": config.compact ?? false,
"pf-m-2-col-on-lg": config.twocolumn ?? false,
"pf-m-3-col-on-lg": config.threecolumn ?? false,
});
return html`

View File

@ -95,7 +95,7 @@ export class NavigationButtons extends AKElement {
);
};
return html`<div class="pf-c-page__header-tools-item pf-m-hidden pf-m-visible-on-lg">
return html`<div class="pf-c-page__header-tools-item pf-m-hidden pf-m-visible-on-xl">
<button class="pf-c-button pf-m-plain" type="button" @click=${onClick}>
<pf-tooltip position="top" content=${msg("Open API drawer")}>
<i class="fas fa-code" aria-hidden="true"></i>
@ -116,7 +116,7 @@ export class NavigationButtons extends AKElement {
);
};
return html`<div class="pf-c-page__header-tools-item pf-m-hidden pf-m-visible-on-lg">
return html`<div class="pf-c-page__header-tools-item pf-m-hidden pf-m-visible-on-xl">
<button
class="pf-c-button pf-m-plain"
type="button"
@ -156,9 +156,7 @@ export class NavigationButtons extends AKElement {
}
renderImpersonation() {
if (!this.me?.original) {
return nothing;
}
if (!this.me?.original) return nothing;
const onClick = async () => {
await new CoreApi(DEFAULT_CONFIG).coreUsersImpersonateEndRetrieve();
@ -175,6 +173,14 @@ export class NavigationButtons extends AKElement {
</div>`;
}
renderAvatar() {
return html`<img
class="pf-c-page__header-tools-item pf-c-avatar pf-m-hidden pf-m-visible-on-xl"
src=${ifDefined(this.me?.user.avatar)}
alt="${msg("Avatar image")}"
/>`;
}
get userDisplayName() {
return match<UserDisplay | undefined, string | undefined>(this.uiConfig?.navbar.userDisplay)
.with(UserDisplay.username, () => this.me?.user.username)
@ -206,17 +212,13 @@ export class NavigationButtons extends AKElement {
</div>
${this.renderImpersonation()}
${this.userDisplayName != ""
? html`<div class="pf-c-page__header-tools-group">
<div class="pf-c-page__header-tools-item pf-m-hidden pf-m-visible-on-md">
? html`<div class="pf-c-page__header-tools-group pf-m-hidden">
<div class="pf-c-page__header-tools-item pf-m-visible-on-2xl">
${this.userDisplayName}
</div>
</div>`
: nothing}
<img
class="pf-c-avatar"
src=${ifDefined(this.me?.user.avatar)}
alt="${msg("Avatar image")}"
/>
${this.renderAvatar()}
</div>`;
}
}

View File

@ -1,4 +1,4 @@
import { convertToSlug } from "@goauthentik/common/utils";
import { formatSlug } from "@goauthentik/elements/router/utils.js";
import { html } from "lit";
import { customElement, property, query } from "lit/decorators.js";
@ -34,7 +34,7 @@ export class AkSlugInput extends HorizontalLightComponent<string> {
// Do not stop propagation of this event; it must be sent up the tree so that a parent
// component, such as a custom forms manager, may receive it.
handleTouch(ev: Event) {
this.input.value = convertToSlug(this.input.value);
this.input.value = formatSlug(this.input.value);
this.value = this.input.value;
if (this.origin && this.origin.value === "" && this.input.value === "") {
@ -67,7 +67,7 @@ export class AkSlugInput extends HorizontalLightComponent<string> {
// "any event which adds or removes a character but leaves the rest of the slug looking like
// the previous iteration, set it to the current iteration."
const newSlug = convertToSlug(ev.target.value);
const newSlug = formatSlug(ev.target.value);
const oldSlug = this.input.value;
const [shorter, longer] =
newSlug.length < oldSlug.length ? [newSlug, oldSlug] : [oldSlug, newSlug];

View File

@ -1,165 +1,140 @@
import { EVENT_THEME_CHANGE } from "@goauthentik/common/constants";
import { globalAK } from "@goauthentik/common/global";
import { UIConfig } from "@goauthentik/common/ui/config";
import { adaptCSS } from "@goauthentik/common/utils";
import { ensureCSSStyleSheet } from "@goauthentik/elements/utils/ensureCSSStyleSheet";
import {
StyleSheetInit,
StyleSheetParent,
appendStyleSheet,
createStyleSheetUnsafe,
removeStyleSheet,
resolveStyleSheetParent,
} from "@goauthentik/common/stylesheets";
import {
CSSColorSchemeValue,
ResolvedUITheme,
UIThemeListener,
createUIThemeEffect,
formatColorScheme,
resolveUITheme,
} from "@goauthentik/common/theme";
import { type ThemedElement } from "@goauthentik/common/theme";
import { localized } from "@lit/localize";
import { LitElement, ReactiveElement } from "lit";
import { CSSResultGroup, CSSResultOrNative, LitElement } from "lit";
import { property } from "lit/decorators.js";
import AKGlobal from "@goauthentik/common/styles/authentik.css";
import OneDark from "@goauthentik/common/styles/one-dark.css";
import ThemeDark from "@goauthentik/common/styles/theme-dark.css";
import { Config, CurrentBrand, UiThemeEnum } from "@goauthentik/api";
import { UiThemeEnum } from "@goauthentik/api";
type AkInterface = HTMLElement & {
getTheme: () => Promise<UiThemeEnum>;
brand?: CurrentBrand;
uiConfig?: UIConfig;
config?: Config;
get activeTheme(): UiThemeEnum | undefined;
};
export const rootInterface = <T extends AkInterface>(): T | undefined =>
(document.body.querySelector("[data-ak-interface-root]") as T) ?? undefined;
export const QUERY_MEDIA_COLOR_LIGHT = "(prefers-color-scheme: light)";
// Ensure themes are converted to a static instance of CSS Stylesheet, otherwise the
// when changing themes we might not remove the correct css stylesheet instance.
const _darkTheme = ensureCSSStyleSheet(ThemeDark);
// Re-export the theme helpers
export { rootInterface } from "@goauthentik/common/theme";
@localized()
export class AKElement extends LitElement {
_mediaMatcher?: MediaQueryList;
_mediaMatcherHandler?: (ev?: MediaQueryListEvent) => void;
_activeTheme?: UiThemeEnum;
export class AKElement extends LitElement implements ThemedElement {
//#region Properties
get activeTheme(): UiThemeEnum | undefined {
return this._activeTheme;
/**
* The resolved theme of the current element.
*
* @remarks
*
* Unlike the browser's current color scheme, this is a value that can be
* resolved to a specific theme, i.e. dark or light.
*/
@property({
attribute: "theme",
type: String,
reflect: true,
})
public activeTheme: ResolvedUITheme;
//#endregion
//#region Private Properties
readonly #preferredColorScheme: CSSColorSchemeValue;
#customCSSStyleSheet: CSSStyleSheet | null;
#darkThemeStyleSheet: CSSStyleSheet | null = null;
#themeAbortController: AbortController | null = null;
//#endregion
//#region Lifecycle
protected static finalizeStyles(styles?: CSSResultGroup): CSSResultOrNative[] {
// Ensure all style sheets being passed are really style sheets.
const baseStyles: StyleSheetInit[] = [AKGlobal, OneDark];
if (!styles) return baseStyles.map(createStyleSheetUnsafe);
if (Array.isArray(styles)) {
return [
//---
...(styles as unknown as CSSResultOrNative[]),
...baseStyles,
].flatMap(createStyleSheetUnsafe);
}
return [styles, ...baseStyles].map(createStyleSheetUnsafe);
}
constructor() {
super();
const { brand } = globalAK();
this.#preferredColorScheme = formatColorScheme(brand.uiTheme);
this.activeTheme = resolveUITheme(brand?.uiTheme);
this.#customCSSStyleSheet = brand?.brandingCustomCss
? createStyleSheetUnsafe(brand.brandingCustomCss)
: null;
}
setInitialStyles(root: DocumentOrShadowRoot) {
const styleRoot: DocumentOrShadowRoot = (
"ShadyDOM" in window ? document : root
) as DocumentOrShadowRoot;
styleRoot.adoptedStyleSheets = adaptCSS([
...styleRoot.adoptedStyleSheets,
ensureCSSStyleSheet(AKGlobal),
ensureCSSStyleSheet(OneDark),
]);
this._initTheme(styleRoot);
this._initCustomCSS(styleRoot);
public disconnectedCallback(): void {
super.disconnectedCallback();
this.#themeAbortController?.abort();
}
protected createRenderRoot() {
this.fixElementStyles();
const root = super.createRenderRoot();
this.setInitialStyles(root as unknown as DocumentOrShadowRoot);
return root;
}
#styleRoot?: StyleSheetParent;
async getTheme(): Promise<UiThemeEnum> {
return rootInterface()?.getTheme() || UiThemeEnum.Automatic;
}
#dispatchTheme: UIThemeListener = (nextUITheme) => {
if (!this.#styleRoot) return;
fixElementStyles() {
// Ensure all style sheets being passed are really style sheets.
(this.constructor as typeof ReactiveElement).elementStyles = (
this.constructor as typeof ReactiveElement
).elementStyles.map(ensureCSSStyleSheet);
}
async _initTheme(root: DocumentOrShadowRoot): Promise<void> {
// Early activate theme based on media query to prevent light flash
// when dark is preferred
this._applyTheme(root, globalAK().brand.uiTheme);
this._applyTheme(root, await this.getTheme());
}
async _initCustomCSS(root: DocumentOrShadowRoot): Promise<void> {
const brand = globalAK().brand;
if (!brand) {
return;
if (nextUITheme === UiThemeEnum.Dark) {
this.#darkThemeStyleSheet ||= createStyleSheetUnsafe(ThemeDark);
appendStyleSheet(this.#styleRoot, this.#darkThemeStyleSheet);
this.activeTheme = UiThemeEnum.Dark;
} else if (this.#darkThemeStyleSheet) {
removeStyleSheet(this.#styleRoot, this.#darkThemeStyleSheet);
this.#darkThemeStyleSheet = null;
this.activeTheme = UiThemeEnum.Light;
}
const sheet = await new CSSStyleSheet().replace(brand.brandingCustomCss);
root.adoptedStyleSheets = [...root.adoptedStyleSheets, sheet];
};
protected createRenderRoot(): HTMLElement | DocumentFragment {
const renderRoot = super.createRenderRoot();
this.#styleRoot = resolveStyleSheetParent(renderRoot);
if (this.#customCSSStyleSheet) {
console.debug(`authentik/element[${this.tagName.toLowerCase()}]: Adding custom CSS`);
appendStyleSheet(this.#styleRoot, this.#customCSSStyleSheet);
}
this.#themeAbortController = new AbortController();
if (this.#preferredColorScheme === "dark") {
this.#dispatchTheme(UiThemeEnum.Dark);
} else if (this.#preferredColorScheme === "auto") {
createUIThemeEffect(this.#dispatchTheme, {
signal: this.#themeAbortController.signal,
});
}
return renderRoot;
}
_applyTheme(root: DocumentOrShadowRoot, theme?: UiThemeEnum): void {
if (!theme) {
theme = UiThemeEnum.Automatic;
}
if (theme === UiThemeEnum.Automatic) {
// Create a media matcher to automatically switch the theme depending on
// prefers-color-scheme
if (!this._mediaMatcher) {
this._mediaMatcher = window.matchMedia(QUERY_MEDIA_COLOR_LIGHT);
this._mediaMatcherHandler = (ev?: MediaQueryListEvent) => {
const theme =
ev?.matches || this._mediaMatcher?.matches
? UiThemeEnum.Light
: UiThemeEnum.Dark;
this._activateTheme(theme, root);
};
this._mediaMatcherHandler(undefined);
this._mediaMatcher.addEventListener("change", this._mediaMatcherHandler);
}
return;
} else if (this._mediaMatcher && this._mediaMatcherHandler) {
// Theme isn't automatic and we have a matcher configured, remove the matcher
// to prevent changes
this._mediaMatcher.removeEventListener("change", this._mediaMatcherHandler);
this._mediaMatcher = undefined;
}
this._activateTheme(theme, root);
}
static themeToStylesheet(theme?: UiThemeEnum): CSSStyleSheet | undefined {
if (theme === UiThemeEnum.Dark) {
return _darkTheme;
}
return undefined;
}
/**
* Directly activate a given theme, accepts multiple document/ShadowDOMs to apply the stylesheet
* to. The stylesheets are applied to each DOM in order. Does nothing if the given theme is already active.
*/
_activateTheme(theme: UiThemeEnum, ...roots: DocumentOrShadowRoot[]) {
if (theme === this._activeTheme) {
return;
}
// Make sure we only get to this callback once we've picked a concise theme choice
this.dispatchEvent(
new CustomEvent(EVENT_THEME_CHANGE, {
bubbles: true,
composed: true,
detail: theme,
}),
);
this.setAttribute("theme", theme);
const stylesheet = AKElement.themeToStylesheet(theme);
const oldStylesheet = AKElement.themeToStylesheet(this._activeTheme);
roots.forEach((root) => {
if (stylesheet) {
root.adoptedStyleSheets = [
...root.adoptedStyleSheets,
ensureCSSStyleSheet(stylesheet),
];
}
if (oldStylesheet) {
root.adoptedStyleSheets = root.adoptedStyleSheets.filter(
(v) => v !== oldStylesheet,
);
}
});
this._activeTheme = theme;
this.requestUpdate();
}
//#endregion
}

View File

@ -1,5 +1,6 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_REFRESH } from "@goauthentik/common/constants";
import { ThemedElement } from "@goauthentik/common/theme";
import { authentikBrandContext } from "@goauthentik/elements/AuthentikContexts";
import type { ReactiveElementHost } from "@goauthentik/elements/types.js";
@ -9,14 +10,12 @@ import type { ReactiveController } from "lit";
import type { CurrentBrand } from "@goauthentik/api";
import { CoreApi } from "@goauthentik/api";
import type { AkInterface } from "./Interface";
export class BrandContextController implements ReactiveController {
host!: ReactiveElementHost<AkInterface>;
host!: ReactiveElementHost<ThemedElement>;
context!: ContextProvider<{ __context__: CurrentBrand | undefined }>;
constructor(host: ReactiveElementHost<AkInterface>) {
constructor(host: ReactiveElementHost<ThemedElement>) {
this.host = host;
this.context = new ContextProvider(this.host, {
context: authentikBrandContext,

View File

@ -1,6 +1,7 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_REFRESH } from "@goauthentik/common/constants";
import { globalAK } from "@goauthentik/common/global";
import { ThemedElement } from "@goauthentik/common/theme";
import { authentikConfigContext } from "@goauthentik/elements/AuthentikContexts";
import type { ReactiveElementHost } from "@goauthentik/elements/types.js";
@ -10,14 +11,12 @@ import type { ReactiveController } from "lit";
import type { Config } from "@goauthentik/api";
import { RootApi } from "@goauthentik/api";
import type { AkInterface } from "./Interface";
export class ConfigContextController implements ReactiveController {
host!: ReactiveElementHost<AkInterface>;
host!: ReactiveElementHost<ThemedElement>;
context!: ContextProvider<{ __context__: Config | undefined }>;
constructor(host: ReactiveElementHost<AkInterface>) {
constructor(host: ReactiveElementHost<ThemedElement>) {
this.host = host;
this.context = new ContextProvider(this.host, {
context: authentikConfigContext,

View File

@ -1,107 +1,78 @@
import { UIConfig, uiConfig } from "@goauthentik/common/ui/config";
import {
appendStyleSheet,
createStyleSheetUnsafe,
resolveStyleSheetParent,
} from "@goauthentik/common/stylesheets";
import { ThemedElement } from "@goauthentik/common/theme";
import { UIConfig } from "@goauthentik/common/ui/config";
import { AKElement } from "@goauthentik/elements/Base";
import { VersionContextController } from "@goauthentik/elements/Interface/VersionContextController";
import { ModalOrchestrationController } from "@goauthentik/elements/controllers/ModalOrchestrationController.js";
import { ensureCSSStyleSheet } from "@goauthentik/elements/utils/ensureCSSStyleSheet";
import { state } from "lit/decorators.js";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import type { Config, CurrentBrand, LicenseSummary, Version } from "@goauthentik/api";
import { UiThemeEnum } from "@goauthentik/api";
import { AKElement, rootInterface } from "../Base";
import { BrandContextController } from "./BrandContextController";
import { ConfigContextController } from "./ConfigContextController";
import { EnterpriseContextController } from "./EnterpriseContextController";
export type AkInterface = HTMLElement & {
getTheme: () => Promise<UiThemeEnum>;
brand?: CurrentBrand;
uiConfig?: UIConfig;
config?: Config;
};
const brandContext = Symbol("brandContext");
const configContext = Symbol("configContext");
const modalController = Symbol("modalController");
const versionContext = Symbol("versionContext");
export class Interface extends AKElement implements AkInterface {
[brandContext]!: BrandContextController;
export abstract class Interface extends AKElement implements ThemedElement {
protected static readonly PFBaseStyleSheet = createStyleSheetUnsafe(PFBase);
[configContext]!: ConfigContextController;
[configContext]: ConfigContextController;
[modalController]!: ModalOrchestrationController;
[modalController]: ModalOrchestrationController;
@state()
uiConfig?: UIConfig;
public config?: Config;
@state()
config?: Config;
@state()
brand?: CurrentBrand;
public brand?: CurrentBrand;
constructor() {
super();
document.adoptedStyleSheets = [...document.adoptedStyleSheets, ensureCSSStyleSheet(PFBase)];
this._initContexts();
this.dataset.akInterfaceRoot = "true";
}
const styleParent = resolveStyleSheetParent(document);
_initContexts() {
this[brandContext] = new BrandContextController(this);
this.dataset.akInterfaceRoot = this.tagName.toLowerCase();
appendStyleSheet(styleParent, Interface.PFBaseStyleSheet);
this.addController(new BrandContextController(this));
this[configContext] = new ConfigContextController(this);
this[modalController] = new ModalOrchestrationController(this);
}
_activateTheme(theme: UiThemeEnum, ...roots: DocumentOrShadowRoot[]): void {
if (theme === this._activeTheme) {
return;
}
console.debug(
`authentik/interface[${rootInterface()?.tagName.toLowerCase()}]: Enabling theme ${theme}`,
);
// Special case for root interfaces, as they need to modify the global document CSS too
// Instead of calling ._activateTheme() twice, we insert the root document in the call
// since multiple calls to ._activateTheme() would not do anything after the first call
// as the theme is already enabled.
roots.unshift(document as unknown as DocumentOrShadowRoot);
super._activateTheme(theme, ...roots);
}
async getTheme(): Promise<UiThemeEnum> {
if (!this.uiConfig) {
this.uiConfig = await uiConfig();
}
return this.uiConfig.theme?.base || UiThemeEnum.Automatic;
}
}
export type AkAuthenticatedInterface = AkInterface & {
export interface AkAuthenticatedInterface extends ThemedElement {
licenseSummary?: LicenseSummary;
version?: Version;
};
}
const enterpriseContext = Symbol("enterpriseContext");
export class AuthenticatedInterface extends Interface {
export class AuthenticatedInterface extends Interface implements AkAuthenticatedInterface {
[enterpriseContext]!: EnterpriseContextController;
[versionContext]!: VersionContextController;
@state()
licenseSummary?: LicenseSummary;
public uiConfig?: UIConfig;
@state()
version?: Version;
public licenseSummary?: LicenseSummary;
@state()
public version?: Version;
constructor() {
super();
}
_initContexts(): void {
super._initContexts();
this[enterpriseContext] = new EnterpriseContextController(this);
this[versionContext] = new VersionContextController(this);
}

View File

@ -4,21 +4,24 @@ import {
TITLE_DEFAULT,
} from "@goauthentik/common/constants";
import { globalAK } from "@goauthentik/common/global";
import { currentInterface } from "@goauthentik/common/sentry";
import { UIConfig, UserDisplay, uiConfig } from "@goauthentik/common/ui/config";
import { UIConfig, UserDisplay, getConfigForUser } from "@goauthentik/common/ui/config";
import { DefaultBrand } from "@goauthentik/common/ui/config";
import { me } from "@goauthentik/common/users";
import "@goauthentik/components/ak-nav-buttons";
import { AKElement } from "@goauthentik/elements/Base";
import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider";
import { isAdminRoute } from "@goauthentik/elements/router/utils";
import { themeImage } from "@goauthentik/elements/utils/images";
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, css, html, nothing } from "lit";
import { CSSResult, LitElement, TemplateResult, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import PFAvatar from "@patternfly/patternfly/components/Avatar/avatar.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFContent from "@patternfly/patternfly/components/Content/content.css";
import PFDrawer from "@patternfly/patternfly/components/Drawer/drawer.css";
import PFDropdown from "@patternfly/patternfly/components/Dropdown/dropdown.css";
import PFNotificationBadge from "@patternfly/patternfly/components/NotificationBadge/notification-badge.css";
import PFPage from "@patternfly/patternfly/components/Page/page.css";
@ -26,34 +29,52 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { SessionUser } from "@goauthentik/api";
@customElement("ak-page-header")
export class PageHeader extends WithBrandConfig(AKElement) {
@property()
icon?: string;
//#region Page Navbar
@property({ type: Boolean })
iconImage = false;
@property()
header = "";
@property()
export interface PageNavbarDetails {
header?: string;
description?: string;
icon?: string;
iconImage?: boolean;
}
@property({ type: Boolean })
hasIcon = true;
/**
* A global navbar component at the top of the page.
*
* Internally, this component listens for the `ak-page-header` event, which is
* dispatched by the `ak-page-header` component.
*/
@customElement("ak-page-navbar")
export class AKPageNavbar extends WithBrandConfig(AKElement) implements PageNavbarDetails {
//#region Static Properties
@state()
me?: SessionUser;
private static elementRef: AKPageNavbar | null = null;
@state()
uiConfig!: UIConfig;
static readonly setNavbarDetails = (detail: Partial<PageNavbarDetails>): void => {
const { elementRef } = AKPageNavbar;
if (!elementRef) {
console.debug(
`ak-page-header: Could not find ak-page-navbar, skipping event dispatch.`,
);
return;
}
const { header, description, icon, iconImage } = detail;
elementRef.header = header;
elementRef.description = description;
elementRef.icon = icon;
elementRef.iconImage = iconImage || false;
elementRef.hasIcon = !!icon;
};
static get styles(): CSSResult[] {
return [
PFBase,
PFButton,
PFPage,
PFDrawer,
PFNotificationBadge,
PFContent,
PFAvatar,
@ -63,143 +84,403 @@ export class PageHeader extends WithBrandConfig(AKElement) {
position: sticky;
top: 0;
z-index: var(--pf-global--ZIndex--lg);
--pf-c-page__header-tools--MarginRight: 0;
--ak-brand-logo-height: var(--pf-global--FontSize--4xl, 2.25rem);
--ak-brand-background-color: var(
--pf-c-page__sidebar--m-light--BackgroundColor
);
--host-navbar-height: var(--ak-c-page-header--height, 7.5rem);
}
.bar {
:host([theme="dark"]) {
--ak-brand-background-color: var(--pf-c-page__sidebar--BackgroundColor);
--pf-c-page__sidebar--BackgroundColor: var(--ak-dark-background-light);
color: var(--ak-dark-foreground);
}
navbar {
border-bottom: var(--pf-global--BorderWidth--sm);
border-bottom-style: solid;
border-bottom-color: var(--pf-global--BorderColor--100);
background-color: var(--pf-c-page--BackgroundColor);
display: flex;
flex-direction: row;
min-height: 114px;
max-height: 114px;
background-color: var(--pf-c-page--BackgroundColor);
display: grid;
row-gap: var(--pf-global--spacer--sm);
column-gap: var(--pf-global--spacer--sm);
grid-template-columns: [brand] auto [toggle] auto [primary] 1fr [secondary] auto;
grid-template-rows: auto auto;
grid-template-areas:
"brand toggle primary secondary"
"brand toggle description secondary";
@media (min-width: 426px) {
height: var(--host-navbar-height);
}
@media (max-width: 768px) {
row-gap: var(--pf-global--spacer--xs);
align-items: center;
grid-template-areas:
"toggle primary secondary"
"toggle description description";
justify-content: space-between;
width: 100%;
}
}
.pf-c-page__main-section.pf-m-light {
background-color: transparent;
.items {
display: block;
&.primary {
grid-column: primary;
grid-row: primary / description;
align-content: center;
padding-block: var(--pf-global--spacer--md);
@media (min-width: 426px) {
&.block-sibling {
padding-block-end: 0;
grid-row: primary;
}
}
@media (max-width: 768px) {
padding-block: var(--pf-global--spacer--sm);
}
.accent-icon {
height: 1em;
width: 1em;
@media (max-width: 768px) {
display: none;
}
}
}
&.page-description {
grid-area: description;
margin-block-end: var(--pf-global--spacer--md);
display: box;
display: -webkit-box;
line-clamp: 2;
-webkit-line-clamp: 2;
box-orient: vertical;
-webkit-box-orient: vertical;
overflow: hidden;
@media (max-width: 425px) {
display: none;
}
@media (min-width: 769px) {
text-wrap: balance;
}
}
&.secondary {
grid-area: secondary;
flex: 0 0 auto;
justify-self: end;
padding-block: var(--pf-global--spacer--sm);
padding-inline-end: var(--pf-global--spacer--sm);
@media (min-width: 769px) {
align-content: center;
padding-block: var(--pf-global--spacer--md);
padding-inline-end: var(--pf-global--spacer--xl);
}
}
}
.pf-c-page__main-section {
flex-grow: 1;
flex-shrink: 1;
.brand {
grid-area: brand;
background-color: var(--ak-brand-background-color);
height: 100%;
width: var(--pf-c-page__sidebar--Width);
align-items: center;
padding-inline: var(--pf-global--spacer--sm);
display: flex;
flex-direction: column;
justify-content: center;
&.pf-m-collapsed {
display: none;
}
@media (max-width: 1199px) {
display: none;
}
}
img.pf-icon {
max-height: 24px;
.sidebar-trigger {
grid-area: toggle;
height: 100%;
}
.logo {
flex: 0 0 auto;
height: var(--ak-brand-logo-height);
& img {
height: 100%;
}
}
.sidebar-trigger,
.notification-trigger {
font-size: 24px;
font-size: 1.5rem;
}
.notification-trigger.has-notifications {
color: var(--pf-global--active-color--100);
}
.page-title {
display: flex;
gap: var(--pf-global--spacer--xs);
}
h1 {
display: flex;
flex-direction: row;
align-items: center !important;
}
.pf-c-page__header-tools {
flex-shrink: 0;
}
.pf-c-page__header-tools-group {
height: 100%;
}
:host([theme="dark"]) .pf-c-page__header-tools {
color: var(--ak-dark-foreground) !important;
}
`,
];
}
constructor() {
super();
window.addEventListener(EVENT_WS_MESSAGE, () => {
this.firstUpdated();
});
}
//#endregion
async firstUpdated() {
this.me = await me();
this.uiConfig = await uiConfig();
this.uiConfig.navbar.userDisplay = UserDisplay.none;
}
//#region Properties
setTitle(header?: string) {
const currentIf = currentInterface();
@property({ type: String })
icon?: string;
@property({ type: Boolean })
iconImage = false;
@property({ type: String })
header?: string;
@property({ type: String })
description?: string;
@property({ type: Boolean })
hasIcon = true;
@property({ type: Boolean })
open = true;
@state()
session?: SessionUser;
@state()
uiConfig!: UIConfig;
//#endregion
//#region Private Methods
#setTitle(header?: string) {
let title = this.brand?.brandingTitle || TITLE_DEFAULT;
if (currentIf === "admin") {
if (isAdminRoute()) {
title = `${msg("Admin")} - ${title}`;
}
// Prepend the header to the title
if (header !== undefined && header !== "") {
if (header) {
title = `${header} - ${title}`;
}
document.title = title;
}
#toggleSidebar() {
this.open = !this.open;
this.dispatchEvent(
new CustomEvent(EVENT_SIDEBAR_TOGGLE, {
bubbles: true,
composed: true,
}),
);
}
//#endregion
//#region Lifecycle
public connectedCallback(): void {
super.connectedCallback();
AKPageNavbar.elementRef = this;
window.addEventListener(EVENT_WS_MESSAGE, () => {
this.firstUpdated();
});
}
public disconnectedCallback(): void {
super.disconnectedCallback();
AKPageNavbar.elementRef = null;
}
public async firstUpdated() {
this.session = await me();
this.uiConfig = getConfigForUser(this.session.user);
this.uiConfig.navbar.userDisplay = UserDisplay.none;
}
willUpdate() {
// Always update title, even if there's no header value set,
// as in that case we still need to return to the generic title
this.setTitle(this.header);
this.#setTitle(this.header);
}
//#endregion
//#region Render
renderIcon() {
if (this.icon) {
if (this.iconImage && !this.icon.startsWith("fa://")) {
return html`<img class="pf-icon" src="${this.icon}" alt="page icon" />`;
return html`<img class="accent-icon pf-icon" src="${this.icon}" alt="page icon" />`;
}
const icon = this.icon.replaceAll("fa://", "fa ");
return html`<i class=${icon}></i>`;
return html`<i class="accent-icon ${icon}"></i>`;
}
return nothing;
}
render(): TemplateResult {
return html`<div class="bar">
<button
class="sidebar-trigger pf-c-button pf-m-plain"
@click=${() => {
this.dispatchEvent(
new CustomEvent(EVENT_SIDEBAR_TOGGLE, {
bubbles: true,
composed: true,
}),
);
}}
>
<i class="fas fa-bars"></i>
</button>
<section class="pf-c-page__main-section pf-m-light">
<div class="pf-c-content">
<h1>
return html`<navbar aria-label="Main" class="navbar">
<aside class="brand ${this.open ? "" : "pf-m-collapsed"}">
<a href="#/">
<div class="logo">
<img
src=${themeImage(
this.brand?.brandingLogo ?? DefaultBrand.brandingLogo,
)}
alt="${msg("authentik Logo")}"
loading="lazy"
/>
</div>
</a>
</aside>
<button
class="sidebar-trigger pf-c-button pf-m-plain"
@click=${this.#toggleSidebar}
aria-label=${msg("Toggle sidebar")}
aria-expanded=${this.open ? "true" : "false"}
>
<i class="fas fa-bars"></i>
</button>
<section
class="items primary pf-c-content ${this.description ? "block-sibling" : ""}"
>
<h1 class="page-title">
${this.hasIcon
? html`<slot name="icon">${this.renderIcon()}</slot>&nbsp;`
? html`<slot name="icon">${this.renderIcon()}</slot>`
: nothing}
<slot name="header">${this.header}</slot>
${this.header}
</h1>
${this.description ? html`<p>${this.description}</p>` : html``}
</div>
</section>
<div class="pf-c-page__header-tools">
<div class="pf-c-page__header-tools-group">
<ak-nav-buttons .uiConfig=${this.uiConfig} .me=${this.me}>
<a
class="pf-c-button pf-m-secondary pf-m-small pf-u-display-none pf-u-display-block-on-md"
href="${globalAK().api.base}if/user/"
slot="extra"
>
${msg("User interface")}
</a>
</ak-nav-buttons>
</div>
</div>
</div>`;
</section>
${this.description
? html`<section class="items page-description pf-c-content">
<p>${this.description}</p>
</section>`
: nothing}
<section class="items secondary">
<div class="pf-c-page__header-tools-group">
<ak-nav-buttons .uiConfig=${this.uiConfig} .me=${this.session}>
<a
class="pf-c-button pf-m-secondary pf-m-small pf-u-display-none pf-u-display-block-on-md"
href="${globalAK().api.base}if/user/"
slot="extra"
>
${msg("User interface")}
</a>
</ak-nav-buttons>
</div>
</section>
</navbar>
<slot></slot>`;
}
//#endregion
}
//#endregion
//#region Page Header
/**
* A page header component, used to display the page title and description.
*
* Internally, this component dispatches the `ak-page-header` event, which is
* listened to by the `ak-page-navbar` component.
*
* @singleton
*/
@customElement("ak-page-header")
export class AKPageHeader extends LitElement implements PageNavbarDetails {
@property({ type: String })
header?: string;
@property({ type: String })
description?: string;
@property({ type: String })
icon?: string;
@property({ type: Boolean })
iconImage = false;
static get styles(): CSSResult[] {
return [
css`
:host {
display: none;
}
`,
];
}
connectedCallback(): void {
super.connectedCallback();
AKPageNavbar.setNavbarDetails({
header: this.header,
description: this.description,
icon: this.icon,
iconImage: this.iconImage,
});
}
updated(): void {
AKPageNavbar.setNavbarDetails({
header: this.header,
description: this.description,
icon: this.icon,
iconImage: this.iconImage,
});
}
}
//#endregion
declare global {
interface HTMLElementTagNameMap {
"ak-page-header": PageHeader;
"ak-page-header": AKPageHeader;
"ak-page-navbar": AKPageNavbar;
}
}

View File

@ -2,11 +2,12 @@ import { EVENT_REFRESH } from "@goauthentik/common/constants";
import { parseAPIResponseError, pluckErrorDetail } from "@goauthentik/common/errors/network";
import { MessageLevel } from "@goauthentik/common/messages";
import { dateToUTC } from "@goauthentik/common/temporal";
import { camelToSnake, convertToSlug } from "@goauthentik/common/utils";
import { camelToSnake } from "@goauthentik/common/utils";
import { AKElement } from "@goauthentik/elements/Base";
import { HorizontalFormElement } from "@goauthentik/elements/forms/HorizontalFormElement";
import { PreventFormSubmit } from "@goauthentik/elements/forms/helpers";
import { showMessage } from "@goauthentik/elements/messages/MessageContainer";
import { formatSlug } from "@goauthentik/elements/router/utils.js";
import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, css, html } from "lit";
@ -218,11 +219,11 @@ export abstract class Form<T> extends AKElement {
// Only attach handler if the slug is already equal to the name
// if not, they are probably completely different and shouldn't update
// each other
if (convertToSlug(input.value) !== slugField.value) {
if (formatSlug(input.value) !== slugField.value) {
return;
}
nameInput.addEventListener("input", () => {
slugField.value = convertToSlug(input.value);
slugField.value = formatSlug(input.value);
});
});
}

View File

@ -1,6 +1,6 @@
import { convertToSlug } from "@goauthentik/common/utils";
import { AKElement } from "@goauthentik/elements/Base";
import { FormGroup } from "@goauthentik/elements/forms/FormGroup";
import { formatSlug } from "@goauthentik/elements/router/utils.js";
import { msg, str } from "@lit/localize";
import { CSSResult, css } from "lit";
@ -123,7 +123,7 @@ export class HorizontalFormElement extends AKElement {
if (this.name === "slug" || this.slugMode) {
this.querySelectorAll<HTMLInputElement>("input[type='text']").forEach((input) => {
input.addEventListener("keyup", () => {
input.value = convertToSlug(input.value);
input.value = formatSlug(input.value);
});
});
}

View File

@ -0,0 +1,74 @@
/**
* @file Utilities for working with the client-side page router.
*/
import { kebabCase } from "change-case";
/**
* The name identifier for the current interface.
*
* @category Routing
*/
export type RouteInterfaceName = "user" | "admin" | "flow" | "unknown";
/**
* Read the current interface route parameter from the URL.
*
* @param location - The location object to read the pathname from. Defaults to `window.location`.
* @returns The name of the current interface, or "unknown" if not found.
*
* @category Routing
*/
export function readInterfaceRouteParam(
location: Pick<URL, "pathname"> = window.location,
): RouteInterfaceName {
const [, currentInterface = "unknown"] = location.pathname.match(/.+if\/(\w+)\//) || [];
return currentInterface.toLowerCase() as RouteInterfaceName;
}
/**
* Predicate to determine if the current route is for the admin interface.
*
* @category Routing
*/
export function isAdminRoute(location: Pick<URL, "pathname"> = window.location): boolean {
return readInterfaceRouteParam(location) === "admin";
}
/**
* Predicate to determine if the current route is for the user interface.
*
* @category Routing
*/
export function isUserRoute(location: Pick<URL, "pathname"> = window.location): boolean {
return readInterfaceRouteParam(location) === "user";
}
/**
* Format a string to a URL-safe route slug.
*
* The input is converted to lowercase and non-alphanumeric characters are
* replaced with a hyphen. Trailing whitespace and hyphens are removed.
*
* @param input - The input string to format.
*
* @category Routing
*
* ```ts
* formatSlug("My Application"); // "my-application"
* formatSlug(" 123ABC "); // "123-ABC"
* formatSlug("-action-Name-"); // "action-name"
* ```
*/
export function formatSlug(input: string): string {
return kebabCase(input);
}
/**
* Predicate to determine if the input is a valid route slug.
*
* @param input - The input string to check.
*/
export function isSlug(input: string): boolean {
return kebabCase(input) === input;
}

View File

@ -1,5 +1,4 @@
import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/sidebar/SidebarBrand";
import "@goauthentik/elements/sidebar/SidebarVersion";
import { msg } from "@lit/localize";
@ -22,6 +21,7 @@ export class Sidebar extends AKElement {
css`
:host {
z-index: 100;
--pf-c-page__sidebar--Transition: 0 !important;
}
.pf-c-nav__link.pf-m-current::after,
.pf-c-nav__link.pf-m-current:hover::after,
@ -35,10 +35,7 @@ export class Sidebar extends AKElement {
.pf-c-nav__section + .pf-c-nav__section {
--pf-c-nav__section--section--MarginTop: var(--pf-global--spacer--sm);
}
.pf-c-nav__list .sidebar-brand {
max-height: 82px;
margin-bottom: -0.5rem;
}
nav {
display: flex;
flex-direction: column;
@ -70,7 +67,6 @@ export class Sidebar extends AKElement {
class="pf-c-nav ${this.activeTheme === UiThemeEnum.Light ? "pf-m-light" : ""}"
aria-label=${msg("Global")}
>
<ak-sidebar-brand></ak-sidebar-brand>
<ul class="pf-c-nav__list">
<slot></slot>
</ul>

View File

@ -1,106 +0,0 @@
import { EVENT_SIDEBAR_TOGGLE } from "@goauthentik/common/constants";
import { globalAK } from "@goauthentik/common/global";
import { AKElement } from "@goauthentik/elements/Base";
import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider";
import { themeImage } from "@goauthentik/elements/utils/images";
import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, css, html, nothing } from "lit";
import { customElement } from "lit/decorators.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFPage from "@patternfly/patternfly/components/Page/page.css";
import PFGlobal from "@patternfly/patternfly/patternfly-base.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { CurrentBrand, UiThemeEnum } from "@goauthentik/api";
// If the viewport is wider than MIN_WIDTH, the sidebar
// is shown besides the content, and not overlaid.
export const MIN_WIDTH = 1200;
export const DefaultBrand: CurrentBrand = {
brandingLogo: "/static/dist/assets/icons/icon_left_brand.svg",
brandingFavicon: "/static/dist/assets/icons/icon.png",
brandingTitle: "authentik",
brandingCustomCss: "",
uiFooterLinks: [],
uiTheme: UiThemeEnum.Automatic,
matchedDomain: "",
defaultLocale: "",
};
@customElement("ak-sidebar-brand")
export class SidebarBrand extends WithBrandConfig(AKElement) {
static get styles(): CSSResult[] {
return [
PFBase,
PFGlobal,
PFPage,
PFButton,
css`
:host {
display: flex;
flex-direction: row;
align-items: center;
height: 114px;
min-height: 114px;
border-bottom: var(--pf-global--BorderWidth--sm);
border-bottom-style: solid;
border-bottom-color: var(--pf-global--BorderColor--100);
}
.pf-c-brand img {
padding: 0 0.5rem;
height: 42px;
}
button.pf-c-button.sidebar-trigger {
background-color: transparent;
border-radius: 0px;
height: 100%;
color: var(--ak-dark-foreground);
}
`,
];
}
constructor() {
super();
window.addEventListener("resize", () => {
this.requestUpdate();
});
}
render(): TemplateResult {
const logoUrl =
globalAK().brand.brandingLogo || this.brand?.brandingLogo || DefaultBrand.brandingLogo;
return html`${window.innerWidth <= MIN_WIDTH
? html`
<button
class="sidebar-trigger pf-c-button"
@click=${() => {
this.dispatchEvent(
new CustomEvent(EVENT_SIDEBAR_TOGGLE, {
bubbles: true,
composed: true,
}),
);
}}
>
<i class="fas fa-bars"></i>
</button>
`
: nothing}
<a href="#/" class="pf-c-page__header-brand-link">
<div class="pf-c-brand ak-brand">
<img src=${themeImage(logoUrl)} alt="${msg("authentik Logo")}" loading="lazy" />
</div>
</a>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-sidebar-brand": SidebarBrand;
}
}

View File

@ -1,9 +1,9 @@
import type { AdminInterface } from "@goauthentik/admin/AdminInterface/index.entrypoint.js";
import { globalAK } from "@goauthentik/common/global";
import { DefaultBrand } from "@goauthentik/common/ui/config";
import { AKElement, rootInterface } from "@goauthentik/elements/Base";
import { WithLicenseSummary } from "@goauthentik/elements/Interface/licenseSummaryProvider";
import { WithVersion } from "@goauthentik/elements/Interface/versionProvider";
import { DefaultBrand } from "@goauthentik/elements/sidebar/SidebarBrand";
import { msg, str } from "@lit/localize";
import { CSSResult, css, html, nothing } from "lit";

View File

@ -1,19 +1,21 @@
import {
appendStyleSheet,
assertAdoptableStyleSheetParent,
createStyleSheetUnsafe,
} from "@goauthentik/common/stylesheets.js";
import { TemplateResult, render as litRender } from "lit";
import AKGlobal from "@goauthentik/common/styles/authentik.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { ensureCSSStyleSheet } from "../utils/ensureCSSStyleSheet.js";
// A special version of render that ensures our style sheets will always be available
// to all elements under test. Ensures they look right during testing, and that any
// CSS-based checks for visibility will return correct values.
export const render = (body: TemplateResult) => {
document.adoptedStyleSheets = [
...document.adoptedStyleSheets,
ensureCSSStyleSheet(PFBase),
ensureCSSStyleSheet(AKGlobal),
];
assertAdoptableStyleSheetParent(document);
appendStyleSheet(document, ...[PFBase, AKGlobal].map(createStyleSheetUnsafe));
return litRender(body, document.body);
};

View File

@ -1,9 +1,14 @@
import { AKElement } from "@goauthentik/elements/Base";
import { type LitElement, type ReactiveControllerHost, type TemplateResult, nothing } from "lit";
import "lit";
export type ReactiveElementHost<T = AKElement> = Partial<ReactiveControllerHost> & T;
/**
* A custom element which may be used as a host for a ReactiveController.
*
* @remarks
*
* This type is derived from an internal type in Lit.
*/
export type ReactiveElementHost<T> = Partial<ReactiveControllerHost & T> & HTMLElement;
export type AbstractLitElementConstructor = abstract new (...args: never[]) => LitElement;

View File

@ -1,35 +0,0 @@
import { CSSResult, unsafeCSS } from "lit";
const supportsAdoptingStyleSheets: boolean =
window.ShadowRoot &&
(window.ShadyCSS === undefined || window.ShadyCSS.nativeShadow) &&
"adoptedStyleSheets" in Document.prototype &&
"replace" in CSSStyleSheet.prototype;
function stringToStylesheet(css: string) {
if (supportsAdoptingStyleSheets) {
const sheet = unsafeCSS(css).styleSheet;
if (sheet === undefined) {
throw new Error(
`CSS processing error: undefined stylesheet from string. Source: ${css}`,
);
}
return sheet;
}
const sheet = new CSSStyleSheet();
sheet.replaceSync(css);
return sheet;
}
function cssResultToStylesheet(css: CSSResult) {
const sheet = css.styleSheet;
return sheet ? sheet : stringToStylesheet(css.toString());
}
export const ensureCSSStyleSheet = (css: string | CSSStyleSheet | CSSResult): CSSStyleSheet =>
css instanceof CSSResult
? cssResultToStylesheet(css)
: typeof css === "string"
? stringToStylesheet(css)
: css;

View File

@ -0,0 +1,55 @@
/**
* @file IFrame Utilities
*/
interface IFrameLoadResult {
contentWindow: Window;
contentDocument: Document;
}
export function pluckIFrameContent(iframe: HTMLIFrameElement) {
const contentWindow = iframe.contentWindow;
const contentDocument = iframe.contentDocument;
if (!contentWindow) {
throw new Error("Iframe contentWindow is not accessible");
}
if (!contentDocument) {
throw new Error("Iframe contentDocument is not accessible");
}
return {
contentWindow,
contentDocument,
};
}
export function resolveIFrameContent(iframe: HTMLIFrameElement): Promise<IFrameLoadResult> {
if (iframe.contentDocument?.readyState === "complete") {
return Promise.resolve(pluckIFrameContent(iframe));
}
return new Promise((resolve) => {
iframe.addEventListener("load", () => resolve(pluckIFrameContent(iframe)), { once: true });
});
}
/**
* Creates a minimal HTML wrapper for an iframe.
*
* @deprecated Use the `contentDocument.body` directly instead.
*/
export function createIFrameHTMLWrapper(bodyContent: string): string {
const html = String.raw;
return html`<!doctype html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body style="display:flex;flex-direction:row;justify-content:center;">
${bodyContent}
</body>
</html>`;
}

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