Compare commits
	
		
			1 Commits
		
	
	
		
			safari-loc
			...
			fix-shared
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 4f98c21f42 | 
							
								
								
									
										2
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								go.mod
									
									
									
									
									
								
							| @ -27,7 +27,7 @@ require ( | |||||||
| 	github.com/spf13/cobra v1.9.1 | 	github.com/spf13/cobra v1.9.1 | ||||||
| 	github.com/stretchr/testify v1.10.0 | 	github.com/stretchr/testify v1.10.0 | ||||||
| 	github.com/wwt/guac v1.3.2 | 	github.com/wwt/guac v1.3.2 | ||||||
| 	goauthentik.io/api/v3 v3.2025024.9 | 	goauthentik.io/api/v3 v3.2025024.8 | ||||||
| 	golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab | 	golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab | ||||||
| 	golang.org/x/oauth2 v0.29.0 | 	golang.org/x/oauth2 v0.29.0 | ||||||
| 	golang.org/x/sync v0.13.0 | 	golang.org/x/sync v0.13.0 | ||||||
|  | |||||||
							
								
								
									
										4
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								go.sum
									
									
									
									
									
								
							| @ -290,8 +290,8 @@ go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y | |||||||
| go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= | go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= | ||||||
| go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= | ||||||
| go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= | ||||||
| goauthentik.io/api/v3 v3.2025024.9 h1:i3tbkyotE32ZpJ729BsPWTuLQUdtZ54Li4aP1amZzsM= | goauthentik.io/api/v3 v3.2025024.8 h1:2mG4CqGSsmZq2CtRehxpDjsER43U/JQSoTOn5VC1ui4= | ||||||
| goauthentik.io/api/v3 v3.2025024.9/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw= | goauthentik.io/api/v3 v3.2025024.8/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw= | ||||||
| golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | ||||||
| golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | ||||||
| golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | ||||||
|  | |||||||
| @ -8,7 +8,7 @@ msgid "" | |||||||
| msgstr "" | msgstr "" | ||||||
| "Project-Id-Version: PACKAGE VERSION\n" | "Project-Id-Version: PACKAGE VERSION\n" | ||||||
| "Report-Msgid-Bugs-To: \n" | "Report-Msgid-Bugs-To: \n" | ||||||
| "POT-Creation-Date: 2025-04-23 09:00+0000\n" | "POT-Creation-Date: 2025-04-22 13:40+0000\n" | ||||||
| "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" | ||||||
| "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" | "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" | ||||||
| "Language-Team: LANGUAGE <LL@li.org>\n" | "Language-Team: LANGUAGE <LL@li.org>\n" | ||||||
| @ -1255,6 +1255,20 @@ msgstr "" | |||||||
| msgid "Reputation Scores" | msgid "Reputation Scores" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | #: authentik/policies/templates/policies/buffer.html | ||||||
|  | msgid "Waiting for authentication..." | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
|  | #: authentik/policies/templates/policies/buffer.html | ||||||
|  | msgid "" | ||||||
|  | "You're already authenticating in another tab. This page will refresh once " | ||||||
|  | "authentication is completed." | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
|  | #: authentik/policies/templates/policies/buffer.html | ||||||
|  | msgid "Authenticate in this tab" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: authentik/policies/templates/policies/denied.html | #: authentik/policies/templates/policies/denied.html | ||||||
| msgid "Permission denied" | msgid "Permission denied" | ||||||
| msgstr "" | msgstr "" | ||||||
|  | |||||||
							
								
								
									
										52
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										52
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -25,7 +25,6 @@ | |||||||
|                 "@formatjs/intl-listformat": "^7.5.7", |                 "@formatjs/intl-listformat": "^7.5.7", | ||||||
|                 "@fortawesome/fontawesome-free": "^6.6.0", |                 "@fortawesome/fontawesome-free": "^6.6.0", | ||||||
|                 "@goauthentik/api": "^2025.2.4-1745325566", |                 "@goauthentik/api": "^2025.2.4-1745325566", | ||||||
|                 "@lit-labs/ssr": "^3.2.2", |  | ||||||
|                 "@lit/context": "^1.1.2", |                 "@lit/context": "^1.1.2", | ||||||
|                 "@lit/localize": "^0.12.2", |                 "@lit/localize": "^0.12.2", | ||||||
|                 "@lit/reactive-element": "^2.0.4", |                 "@lit/reactive-element": "^2.0.4", | ||||||
| @ -66,6 +65,7 @@ | |||||||
|                 "remark-gfm": "^4.0.1", |                 "remark-gfm": "^4.0.1", | ||||||
|                 "remark-mdx-frontmatter": "^5.0.0", |                 "remark-mdx-frontmatter": "^5.0.0", | ||||||
|                 "style-mod": "^4.1.2", |                 "style-mod": "^4.1.2", | ||||||
|  |                 "trusted-types": "^2.0.0", | ||||||
|                 "ts-pattern": "^5.4.0", |                 "ts-pattern": "^5.4.0", | ||||||
|                 "unist-util-visit": "^5.0.0", |                 "unist-util-visit": "^5.0.0", | ||||||
|                 "webcomponent-qr-code": "^1.2.0", |                 "webcomponent-qr-code": "^1.2.0", | ||||||
| @ -2281,47 +2281,11 @@ | |||||||
|                 "@lezer/lr": "^1.0.0" |                 "@lezer/lr": "^1.0.0" | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/@lit-labs/ssr": { |  | ||||||
|             "version": "3.3.1", |  | ||||||
|             "resolved": "https://registry.npmjs.org/@lit-labs/ssr/-/ssr-3.3.1.tgz", |  | ||||||
|             "integrity": "sha512-JlF1PempxvzrGEpRFrF+Ki0MHzR3HA51SK8Zv0cFpW9p0bPW4k0FeCwrElCu371UEpXF7RcaE2wgYaE1az0XKg==", |  | ||||||
|             "dependencies": { |  | ||||||
|                 "@lit-labs/ssr-client": "^1.1.7", |  | ||||||
|                 "@lit-labs/ssr-dom-shim": "^1.3.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==", |  | ||||||
|             "dependencies": { |  | ||||||
|                 "@lit/reactive-element": "^2.0.4", |  | ||||||
|                 "lit": "^3.1.2", |  | ||||||
|                 "lit-html": "^3.1.2" |  | ||||||
|             } |  | ||||||
|         }, |  | ||||||
|         "node_modules/@lit-labs/ssr-dom-shim": { |         "node_modules/@lit-labs/ssr-dom-shim": { | ||||||
|             "version": "1.3.0", |             "version": "1.3.0", | ||||||
|             "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.3.0.tgz", |             "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.3.0.tgz", | ||||||
|             "integrity": "sha512-nQIWonJ6eFAvUUrSlwyHDm/aE8PBDu5kRpL0vHMg6K8fK3Diq1xdPjTnsJSwxABhaZ+5eBi1btQB5ShUTKo4nQ==" |             "integrity": "sha512-nQIWonJ6eFAvUUrSlwyHDm/aE8PBDu5kRpL0vHMg6K8fK3Diq1xdPjTnsJSwxABhaZ+5eBi1btQB5ShUTKo4nQ==" | ||||||
|         }, |         }, | ||||||
|         "node_modules/@lit-labs/ssr/node_modules/@types/node": { |  | ||||||
|             "version": "16.18.126", |  | ||||||
|             "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.126.tgz", |  | ||||||
|             "integrity": "sha512-OTcgaiwfGFBKacvfwuHzzn1KLxH/er8mluiy8/uM3sGXHaRe73RrSIj01jow9t4kJEW633Ov+cOexXeiApTyAw==" |  | ||||||
|         }, |  | ||||||
|         "node_modules/@lit/context": { |         "node_modules/@lit/context": { | ||||||
|             "version": "1.1.5", |             "version": "1.1.5", | ||||||
|             "resolved": "https://registry.npmjs.org/@lit/context/-/context-1.1.5.tgz", |             "resolved": "https://registry.npmjs.org/@lit/context/-/context-1.1.5.tgz", | ||||||
| @ -3557,6 +3521,7 @@ | |||||||
|             "version": "0.3.0", |             "version": "0.3.0", | ||||||
|             "resolved": "https://registry.npmjs.org/@parse5/tools/-/tools-0.3.0.tgz", |             "resolved": "https://registry.npmjs.org/@parse5/tools/-/tools-0.3.0.tgz", | ||||||
|             "integrity": "sha512-zxRyTHkqb7WQMV8kTNBKWb1BeOFUKXBXTBWuxg9H9hfvQB3IwP6Iw2U75Ia5eyRxPNltmY7E8YAlz6zWwUnjKg==", |             "integrity": "sha512-zxRyTHkqb7WQMV8kTNBKWb1BeOFUKXBXTBWuxg9H9hfvQB3IwP6Iw2U75Ia5eyRxPNltmY7E8YAlz6zWwUnjKg==", | ||||||
|  |             "dev": true, | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "parse5": "^7.0.0" |                 "parse5": "^7.0.0" | ||||||
|             } |             } | ||||||
| @ -10723,6 +10688,7 @@ | |||||||
|             "version": "4.0.1", |             "version": "4.0.1", | ||||||
|             "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", |             "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", | ||||||
|             "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", |             "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", | ||||||
|  |             "dev": true, | ||||||
|             "engines": { |             "engines": { | ||||||
|                 "node": ">= 12" |                 "node": ">= 12" | ||||||
|             } |             } | ||||||
| @ -11343,6 +11309,7 @@ | |||||||
|             "version": "5.18.1", |             "version": "5.18.1", | ||||||
|             "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", |             "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", | ||||||
|             "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", |             "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", | ||||||
|  |             "dev": true, | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "graceful-fs": "^4.2.4", |                 "graceful-fs": "^4.2.4", | ||||||
|                 "tapable": "^2.2.0" |                 "tapable": "^2.2.0" | ||||||
| @ -13820,7 +13787,8 @@ | |||||||
|         "node_modules/graceful-fs": { |         "node_modules/graceful-fs": { | ||||||
|             "version": "4.2.11", |             "version": "4.2.11", | ||||||
|             "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", |             "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": { |         "node_modules/grapheme-splitter": { | ||||||
|             "version": "1.0.4", |             "version": "1.0.4", | ||||||
| @ -18256,6 +18224,7 @@ | |||||||
|             "version": "3.3.2", |             "version": "3.3.2", | ||||||
|             "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", |             "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", | ||||||
|             "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", |             "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", | ||||||
|  |             "dev": true, | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "data-uri-to-buffer": "^4.0.0", |                 "data-uri-to-buffer": "^4.0.0", | ||||||
|                 "fetch-blob": "^3.1.4", |                 "fetch-blob": "^3.1.4", | ||||||
| @ -22373,6 +22342,7 @@ | |||||||
|             "version": "2.2.1", |             "version": "2.2.1", | ||||||
|             "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", |             "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", | ||||||
|             "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", |             "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", | ||||||
|  |             "dev": true, | ||||||
|             "engines": { |             "engines": { | ||||||
|                 "node": ">=6" |                 "node": ">=6" | ||||||
|             } |             } | ||||||
| @ -22724,6 +22694,12 @@ | |||||||
|                 "url": "https://github.com/sponsors/wooorm" |                 "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": { |         "node_modules/ts-api-utils": { | ||||||
|             "version": "2.1.0", |             "version": "2.1.0", | ||||||
|             "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", |             "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", | ||||||
|  | |||||||
| @ -13,7 +13,6 @@ | |||||||
|         "@formatjs/intl-listformat": "^7.5.7", |         "@formatjs/intl-listformat": "^7.5.7", | ||||||
|         "@fortawesome/fontawesome-free": "^6.6.0", |         "@fortawesome/fontawesome-free": "^6.6.0", | ||||||
|         "@goauthentik/api": "^2025.2.4-1745325566", |         "@goauthentik/api": "^2025.2.4-1745325566", | ||||||
|         "@lit-labs/ssr": "^3.2.2", |  | ||||||
|         "@lit/context": "^1.1.2", |         "@lit/context": "^1.1.2", | ||||||
|         "@lit/localize": "^0.12.2", |         "@lit/localize": "^0.12.2", | ||||||
|         "@lit/reactive-element": "^2.0.4", |         "@lit/reactive-element": "^2.0.4", | ||||||
| @ -54,6 +53,7 @@ | |||||||
|         "remark-gfm": "^4.0.1", |         "remark-gfm": "^4.0.1", | ||||||
|         "remark-mdx-frontmatter": "^5.0.0", |         "remark-mdx-frontmatter": "^5.0.0", | ||||||
|         "style-mod": "^4.1.2", |         "style-mod": "^4.1.2", | ||||||
|  |         "trusted-types": "^2.0.0", | ||||||
|         "ts-pattern": "^5.4.0", |         "ts-pattern": "^5.4.0", | ||||||
|         "unist-util-visit": "^5.0.0", |         "unist-util-visit": "^5.0.0", | ||||||
|         "webcomponent-qr-code": "^1.2.0", |         "webcomponent-qr-code": "^1.2.0", | ||||||
|  | |||||||
| @ -4,12 +4,8 @@ import { | |||||||
|     EventMiddleware, |     EventMiddleware, | ||||||
|     LoggingMiddleware, |     LoggingMiddleware, | ||||||
| } from "@goauthentik/common/api/middleware"; | } from "@goauthentik/common/api/middleware"; | ||||||
| import { VERSION } from "@goauthentik/common/constants"; | import { EVENT_LOCALE_REQUEST, VERSION } from "@goauthentik/common/constants"; | ||||||
| import { globalAK } from "@goauthentik/common/global"; | import { globalAK } from "@goauthentik/common/global"; | ||||||
| import { |  | ||||||
|     EVENT_LOCALE_REQUEST, |  | ||||||
|     LocaleContextEventDetail, |  | ||||||
| } from "@goauthentik/elements/ak-locale-context/events.js"; |  | ||||||
|  |  | ||||||
| import { Config, Configuration, CoreApi, CurrentBrand, RootApi } from "@goauthentik/api"; | import { Config, Configuration, CoreApi, CurrentBrand, RootApi } from "@goauthentik/api"; | ||||||
|  |  | ||||||
| @ -48,7 +44,7 @@ export function brandSetLocale(brand: CurrentBrand) { | |||||||
|     } |     } | ||||||
|     console.debug("authentik/locale: setting locale from brand default"); |     console.debug("authentik/locale: setting locale from brand default"); | ||||||
|     window.dispatchEvent( |     window.dispatchEvent( | ||||||
|         new CustomEvent<LocaleContextEventDetail>(EVENT_LOCALE_REQUEST, { |         new CustomEvent(EVENT_LOCALE_REQUEST, { | ||||||
|             composed: true, |             composed: true, | ||||||
|             bubbles: true, |             bubbles: true, | ||||||
|             detail: { locale: brand.defaultLocale }, |             detail: { locale: brand.defaultLocale }, | ||||||
|  | |||||||
| @ -14,6 +14,8 @@ export const EVENT_FLOW_INSPECTOR_TOGGLE = "ak-flow-inspector-toggle"; | |||||||
| export const EVENT_SIDEBAR_TOGGLE = "ak-sidebar-toggle"; | export const EVENT_SIDEBAR_TOGGLE = "ak-sidebar-toggle"; | ||||||
| export const EVENT_WS_MESSAGE = "ak-ws-message"; | export const EVENT_WS_MESSAGE = "ak-ws-message"; | ||||||
| export const EVENT_FLOW_ADVANCE = "ak-flow-advance"; | export const EVENT_FLOW_ADVANCE = "ak-flow-advance"; | ||||||
|  | export const EVENT_LOCALE_CHANGE = "ak-locale-change"; | ||||||
|  | export const EVENT_LOCALE_REQUEST = "ak-locale-request"; | ||||||
| export const EVENT_REQUEST_POST = "ak-request-post"; | export const EVENT_REQUEST_POST = "ak-request-post"; | ||||||
| export const EVENT_MESSAGE = "ak-message"; | export const EVENT_MESSAGE = "ak-message"; | ||||||
| export const EVENT_THEME_CHANGE = "ak-theme-change"; | export const EVENT_THEME_CHANGE = "ak-theme-change"; | ||||||
|  | |||||||
| @ -1,26 +1,110 @@ | |||||||
| import type { Config as DOMPurifyConfig } from "dompurify"; | import type { Config as DOMPurifyConfig } from "dompurify"; | ||||||
| import DOMPurify from "dompurify"; | import DOMPurify from "dompurify"; | ||||||
|  | import { trustedTypes } from "trusted-types"; | ||||||
|  |  | ||||||
| import { render } from "@lit-labs/ssr"; | import { render } from "lit"; | ||||||
| import { collectResult } from "@lit-labs/ssr/lib/render-result.js"; |  | ||||||
| import { TemplateResult, html } from "lit"; |  | ||||||
| import { unsafeHTML } from "lit/directives/unsafe-html.js"; | 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 = { | export const DOM_PURIFY_STRICT = { | ||||||
|     ALLOWED_TAGS: ["#text"], |     ALLOWED_TAGS: ["#text"], | ||||||
| } as const satisfies DOMPurifyConfig; | } 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 { |     const result = container.innerHTML; | ||||||
|     return html`${until( |  | ||||||
|         (async () => { |     return result; | ||||||
|             const rendered = await renderStatic(input); |  | ||||||
|             const purified = DOMPurify.sanitize(rendered); |  | ||||||
|             return html`${unsafeHTML(purified)}`; |  | ||||||
|         })(), |  | ||||||
|     )}`; |  | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,9 +1,6 @@ | |||||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||||
|  | import { EVENT_LOCALE_REQUEST } from "@goauthentik/common/constants"; | ||||||
| import { isResponseErrorLike } from "@goauthentik/common/errors/network"; | import { isResponseErrorLike } from "@goauthentik/common/errors/network"; | ||||||
| import { |  | ||||||
|     EVENT_LOCALE_REQUEST, |  | ||||||
|     LocaleContextEventDetail, |  | ||||||
| } from "@goauthentik/elements/ak-locale-context/events.js"; |  | ||||||
|  |  | ||||||
| import { CoreApi, SessionUser } from "@goauthentik/api"; | import { CoreApi, SessionUser } from "@goauthentik/api"; | ||||||
|  |  | ||||||
| @ -60,7 +57,7 @@ export async function me(): Promise<SessionUser> { | |||||||
|                 console.debug(`authentik/locale: Activating user's configured locale '${locale}'`); |                 console.debug(`authentik/locale: Activating user's configured locale '${locale}'`); | ||||||
|  |  | ||||||
|                 window.dispatchEvent( |                 window.dispatchEvent( | ||||||
|                     new CustomEvent<LocaleContextEventDetail>(EVENT_LOCALE_REQUEST, { |                     new CustomEvent(EVENT_LOCALE_REQUEST, { | ||||||
|                         composed: true, |                         composed: true, | ||||||
|                         bubbles: true, |                         bubbles: true, | ||||||
|                         detail: { locale }, |                         detail: { locale }, | ||||||
|  | |||||||
| @ -1,9 +1,11 @@ | |||||||
|  | import { EVENT_LOCALE_REQUEST } from "@goauthentik/common/constants"; | ||||||
|  | import { customEvent } from "@goauthentik/elements/utils/customEvents"; | ||||||
|  |  | ||||||
| import { localized, msg } from "@lit/localize"; | import { localized, msg } from "@lit/localize"; | ||||||
| import { LitElement, html } from "lit"; | import { LitElement, html } from "lit"; | ||||||
| import { customElement } from "lit/decorators.js"; | import { customElement } from "lit/decorators.js"; | ||||||
|  |  | ||||||
| import "./ak-locale-context"; | import "./ak-locale-context"; | ||||||
| import { EVENT_LOCALE_REQUEST, LocaleContextEventDetail } from "./events.js"; |  | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|     title: "Elements / Shell / Locale Context", |     title: "Elements / Shell / Locale Context", | ||||||
| @ -35,18 +37,10 @@ export const InFrench = () => | |||||||
|     </div>`; |     </div>`; | ||||||
|  |  | ||||||
| export const SwitchingBackAndForth = () => { | export const SwitchingBackAndForth = () => { | ||||||
|     let languageCode = "en"; |     let lang = "en"; | ||||||
|  |  | ||||||
|     window.setInterval(() => { |     window.setInterval(() => { | ||||||
|         languageCode = languageCode === "en" ? "fr" : "en"; |         lang = lang === "en" ? "fr" : "en"; | ||||||
|  |         window.dispatchEvent(customEvent(EVENT_LOCALE_REQUEST, { locale: lang })); | ||||||
|         window.dispatchEvent( |  | ||||||
|             new CustomEvent<LocaleContextEventDetail>(EVENT_LOCALE_REQUEST, { |  | ||||||
|                 composed: true, |  | ||||||
|                 bubbles: true, |  | ||||||
|                 detail: { locale: languageCode }, |  | ||||||
|             }), |  | ||||||
|         ); |  | ||||||
|     }, 1000); |     }, 1000); | ||||||
|  |  | ||||||
|     return html`<div style="background: #fff; padding: 4em"> |     return html`<div style="background: #fff; padding: 4em"> | ||||||
|  | |||||||
| @ -1,18 +1,19 @@ | |||||||
|  | import { EVENT_LOCALE_CHANGE, EVENT_LOCALE_REQUEST } from "@goauthentik/common/constants"; | ||||||
| import { AKElement } from "@goauthentik/elements/Base"; | import { AKElement } from "@goauthentik/elements/Base"; | ||||||
|  | import { customEvent } from "@goauthentik/elements/utils/customEvents"; | ||||||
|  |  | ||||||
| import { html } from "lit"; | import { html } from "lit"; | ||||||
| import { customElement, property } from "lit/decorators.js"; | import { customElement, property } from "lit/decorators.js"; | ||||||
|  |  | ||||||
| import { WithBrandConfig } from "../Interface/brandProvider"; | import { WithBrandConfig } from "../Interface/brandProvider"; | ||||||
| import { initializeLocalization } from "./configureLocale.js"; | import { initializeLocalization } from "./configureLocale"; | ||||||
| import type { GetLocale, SetLocale } from "./configureLocale.js"; | import type { LocaleGetter, LocaleSetter } from "./configureLocale"; | ||||||
| import { EVENT_LOCALE_CHANGE, EVENT_LOCALE_REQUEST, LocaleContextEventDetail } from "./events.js"; | import { DEFAULT_LOCALE, autoDetectLanguage, getBestMatchLocale } from "./helpers"; | ||||||
| import { DEFAULT_LOCALE, autoDetectLanguage, findLocaleDefinition } from "./helpers.js"; |  | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * A component to manage your locale settings. |  * A component to manage your locale settings. | ||||||
|  * |  * | ||||||
|  * @remarks |  * ## Details | ||||||
|  * |  * | ||||||
|  * This component exists to take a locale setting from several different places, find the |  * This component exists to take a locale setting from several different places, find the | ||||||
|  * appropriate locale file in our catalog of locales, and set the lit-localization context |  * appropriate locale file in our catalog of locales, and set the lit-localization context | ||||||
| @ -24,98 +25,70 @@ import { DEFAULT_LOCALE, autoDetectLanguage, findLocaleDefinition } from "./help | |||||||
|  */ |  */ | ||||||
| @customElement("ak-locale-context") | @customElement("ak-locale-context") | ||||||
| export class LocaleContext extends WithBrandConfig(AKElement) { | export class LocaleContext extends WithBrandConfig(AKElement) { | ||||||
|     protected static singleton: LocaleContext | null = null; |     /// @attribute The text representation of the current locale */ | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * The text representation of the current locale |  | ||||||
|      * @attribute |  | ||||||
|      */ |  | ||||||
|     @property({ attribute: true, type: String }) |     @property({ attribute: true, type: String }) | ||||||
|     public locale = DEFAULT_LOCALE; |     locale = DEFAULT_LOCALE; | ||||||
|  |  | ||||||
|     /** |     /// @attribute The URL parameter to look for (if any) | ||||||
|      * The URL parameter to look for (if any) |  | ||||||
|      * @attribute |  | ||||||
|      */ |  | ||||||
|     @property({ attribute: true, type: String }) |     @property({ attribute: true, type: String }) | ||||||
|     public param = "locale"; |     param = "locale"; | ||||||
|  |  | ||||||
|     protected readonly getLocale: GetLocale; |     getLocale: LocaleGetter; | ||||||
|     protected readonly setLocale: SetLocale; |  | ||||||
|  |     setLocale: LocaleSetter; | ||||||
|  |  | ||||||
|     constructor(code = DEFAULT_LOCALE) { |     constructor(code = DEFAULT_LOCALE) { | ||||||
|         super(); |         super(); | ||||||
|  |         this.notifyApplication = this.notifyApplication.bind(this); | ||||||
|         if (LocaleContext.singleton) { |         this.updateLocaleHandler = this.updateLocaleHandler.bind(this); | ||||||
|             throw new Error(`Developer error: Must have only one locale context per session`); |         try { | ||||||
|  |             const [getLocale, setLocale] = initializeLocalization(); | ||||||
|  |             this.getLocale = getLocale; | ||||||
|  |             this.setLocale = setLocale; | ||||||
|  |             this.setLocale(code).then(() => { | ||||||
|  |                 window.setTimeout(this.notifyApplication, 0); | ||||||
|  |             }); | ||||||
|  |         } catch (e) { | ||||||
|  |             throw new Error(`Developer error: Must have only one locale context per session: ${e}`); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         LocaleContext.singleton = this; |  | ||||||
|  |  | ||||||
|         const [getLocale, setLocale] = initializeLocalization(); |  | ||||||
|  |  | ||||||
|         this.getLocale = getLocale; |  | ||||||
|         this.setLocale = setLocale; |  | ||||||
|  |  | ||||||
|         this.setLocale(code).then(this.#notifyApplication); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     connectedCallback() { |     connectedCallback() { | ||||||
|         this.#updateLocale(); |         super.connectedCallback(); | ||||||
|  |         this.updateLocale(); | ||||||
|         window.addEventListener(EVENT_LOCALE_REQUEST, this.#localeUpdateListener as EventListener); |         window.addEventListener(EVENT_LOCALE_REQUEST, this.updateLocaleHandler as EventListener); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     disconnectedCallback() { |     disconnectedCallback() { | ||||||
|         LocaleContext.singleton = null; |         window.removeEventListener(EVENT_LOCALE_REQUEST, this.updateLocaleHandler as EventListener); | ||||||
|  |  | ||||||
|         window.removeEventListener( |  | ||||||
|             EVENT_LOCALE_REQUEST, |  | ||||||
|             this.#localeUpdateListener as EventListener, |  | ||||||
|         ); |  | ||||||
|         super.disconnectedCallback(); |         super.disconnectedCallback(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     #localeUpdateListener = (ev: CustomEvent<LocaleContextEventDetail>) => { |     updateLocaleHandler(ev: CustomEvent<{ locale: string }>) { | ||||||
|         console.debug("authentik/locale: Locale update request received."); |         console.debug("authentik/locale: Locale update request received."); | ||||||
|         this.#updateLocale(ev.detail.locale); |         this.updateLocale(ev.detail.locale); | ||||||
|     }; |     } | ||||||
|  |  | ||||||
|     #updateLocale(requestedLanguageCode?: string) { |  | ||||||
|         const localeRequest = autoDetectLanguage(requestedLanguageCode, this.brand?.defaultLocale); |  | ||||||
|  |  | ||||||
|         const locale = findLocaleDefinition(localeRequest); |  | ||||||
|  |  | ||||||
|  |     updateLocale(requestedLocale: string | undefined = undefined) { | ||||||
|  |         const localeRequest = autoDetectLanguage(requestedLocale, this.brand?.defaultLocale); | ||||||
|  |         const locale = getBestMatchLocale(localeRequest); | ||||||
|         if (!locale) { |         if (!locale) { | ||||||
|             console.warn(`authentik/locale: failed to find locale for code ${localeRequest}`); |             console.warn(`authentik/locale: failed to find locale for code ${localeRequest}`); | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|  |         locale.locale().then(() => { | ||||||
|         return locale.fetch().then(() => { |             console.debug(`authentik/locale: Setting Locale to ${locale.label()} (${locale.code})`); | ||||||
|             console.debug( |             this.setLocale(locale.code).then(() => { | ||||||
|                 `authentik/locale: Setting Locale to ${locale.formatLabel()} (${locale.languageCode})`, |                 window.setTimeout(this.notifyApplication, 0); | ||||||
|             ); |             }); | ||||||
|  |  | ||||||
|             this.setLocale(locale.languageCode).then(this.#notifyApplication); |  | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     #notifyFrameID = -1; |     notifyApplication() { | ||||||
|  |         // You will almost never have cause to catch this event. Lit's own `@localized()` decorator | ||||||
|     #notifyApplication = () => { |         // works just fine for almost every use case. | ||||||
|         cancelAnimationFrame(this.#notifyFrameID); |         this.dispatchEvent(customEvent(EVENT_LOCALE_CHANGE)); | ||||||
|  |     } | ||||||
|         requestAnimationFrame(() => { |  | ||||||
|             // You will almost never have cause to catch this event. |  | ||||||
|             // Lit's own `@localized()` decorator works just fine for almost every use case. |  | ||||||
|             this.dispatchEvent( |  | ||||||
|                 new CustomEvent(EVENT_LOCALE_CHANGE, { |  | ||||||
|                     bubbles: true, |  | ||||||
|                     composed: true, |  | ||||||
|                 }), |  | ||||||
|             ); |  | ||||||
|         }); |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|     render() { |     render() { | ||||||
|         return html`<slot></slot>`; |         return html`<slot></slot>`; | ||||||
|  | |||||||
| @ -1,44 +1,39 @@ | |||||||
| import { configureLocalization } from "@lit/localize"; | import { configureLocalization } from "@lit/localize"; | ||||||
|  |  | ||||||
| import { sourceLocale, targetLocales } from "../../locale-codes.js"; | import { sourceLocale, targetLocales } from "../../locale-codes"; | ||||||
| import { findLocaleDefinition } from "./helpers.js"; | import { getBestMatchLocale } from "./helpers"; | ||||||
|  |  | ||||||
| export type ConfigureLocalizationResult = ReturnType<typeof configureLocalization>; | type LocaleGetter = ReturnType<typeof configureLocalization>["getLocale"]; | ||||||
|  | type LocaleSetter = ReturnType<typeof configureLocalization>["setLocale"]; | ||||||
|  |  | ||||||
| export type GetLocale = ConfigureLocalizationResult["getLocale"]; | // Internal use only. | ||||||
| export type SetLocale = ConfigureLocalizationResult["setLocale"]; | // | ||||||
|  | // This is where the lit-localization module is initialized with our loader, which associates our | ||||||
|  | // collection of locales with its getter and setter functions. | ||||||
|  |  | ||||||
| export type LocaleState = [GetLocale, SetLocale]; | let getLocale: LocaleGetter | undefined = undefined; | ||||||
|  | let setLocale: LocaleSetter | undefined = undefined; | ||||||
|  |  | ||||||
| let cachedLocaleState: LocaleState | undefined = undefined; | export function initializeLocalization(): [LocaleGetter, LocaleSetter] { | ||||||
|  |     if (getLocale && setLocale) { | ||||||
|  |         return [getLocale, setLocale]; | ||||||
|  |     } | ||||||
|  |  | ||||||
| /** |     ({ getLocale, setLocale } = configureLocalization({ | ||||||
|  * This is where the lit-localization module is initialized with our loader, |  | ||||||
|  * which associates our collection of locales with its getter and setter functions. |  | ||||||
|  * |  | ||||||
|  * @returns A tuple of getter and setter functions. |  | ||||||
|  * @internal |  | ||||||
|  */ |  | ||||||
| export function initializeLocalization(): LocaleState { |  | ||||||
|     if (cachedLocaleState) return cachedLocaleState; |  | ||||||
|  |  | ||||||
|     const { getLocale, setLocale } = configureLocalization({ |  | ||||||
|         sourceLocale, |         sourceLocale, | ||||||
|         targetLocales, |         targetLocales, | ||||||
|         loadLocale: (languageCode) => { |         loadLocale: async (locale: string) => { | ||||||
|             const localeDef = findLocaleDefinition(languageCode); |             const localeDef = getBestMatchLocale(locale); | ||||||
|  |  | ||||||
|             if (!localeDef) { |             if (!localeDef) { | ||||||
|                 throw new Error(`Unrecognized locale: ${localeDef}`); |                 console.warn(`Unrecognized locale: ${localeDef}`); | ||||||
|  |                 return Promise.reject(""); | ||||||
|             } |             } | ||||||
|  |             return localeDef.locale(); | ||||||
|             return localeDef.fetch(); |  | ||||||
|         }, |         }, | ||||||
|     }); |     })); | ||||||
|  |  | ||||||
|     cachedLocaleState = [getLocale, setLocale]; |     return [getLocale, setLocale]; | ||||||
|  |  | ||||||
|     return cachedLocaleState; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| export default initializeLocalization; | export default initializeLocalization; | ||||||
|  | export type { LocaleGetter, LocaleSetter }; | ||||||
|  | |||||||
| @ -1,19 +1,15 @@ | |||||||
| import * as EnglishLocaleModule from "@goauthentik/locales/en"; | import * as _enLocale from "@goauthentik/locales/en"; | ||||||
|  |  | ||||||
| import type { LocaleModule } from "@lit/localize"; | import type { LocaleModule } from "@lit/localize"; | ||||||
| import { msg } from "@lit/localize"; | import { msg } from "@lit/localize"; | ||||||
|  |  | ||||||
| import { AKLocaleDefinition, LocaleRow } from "./types.js"; | import { AkLocale, LocaleRow } from "./types"; | ||||||
|  |  | ||||||
| /** | export const DEFAULT_FALLBACK = "en"; | ||||||
|  * The default ISO 639-1 language code. |  | ||||||
|  */ |  | ||||||
| export const DEFAULT_LANGUAGE_CODE = "en"; |  | ||||||
|  |  | ||||||
| /** | const enLocale: LocaleModule = _enLocale; | ||||||
|  * The default English locale module. |  | ||||||
|  */ | export { enLocale }; | ||||||
| export const DefaultLocaleModule: LocaleModule = EnglishLocaleModule; |  | ||||||
|  |  | ||||||
| // NOTE: This table cannot be made any shorter, despite all the repetition of syntax. Bundlers look | // NOTE: This table cannot be made any shorter, despite all the repetition of syntax. Bundlers look | ||||||
| // for the `await import` string as a *string target* for doing alias substitution, so putting | // for the `await import` string as a *string target* for doing alias substitution, so putting | ||||||
| @ -39,44 +35,34 @@ export const DefaultLocaleModule: LocaleModule = EnglishLocaleModule; | |||||||
| // - Text Label | // - Text Label | ||||||
| // - Locale loader. | // - Locale loader. | ||||||
|  |  | ||||||
|  | // prettier-ignore | ||||||
| const debug: LocaleRow = [ | const debug: LocaleRow = [ | ||||||
|     "pseudo-LOCALE", |     "pseudo-LOCALE",  /^pseudo/i,  () => msg("Pseudolocale (for testing)"),  async () => await import("@goauthentik/locales/pseudo-LOCALE"), | ||||||
|     /^pseudo/i, |  | ||||||
|     () => msg("Pseudolocale (for testing)"), |  | ||||||
|     () => import("@goauthentik/locales/pseudo-LOCALE"), |  | ||||||
| ]; | ]; | ||||||
|  |  | ||||||
| // prettier-ignore | // prettier-ignore | ||||||
| const LOCALE_TABLE: readonly LocaleRow[] = [ | const LOCALE_TABLE: LocaleRow[] = [ | ||||||
|     // English loaded when the application is first instantiated. |     ["de",      /^de([_-]|$)/i,      () => msg("German"),                async () => await import("@goauthentik/locales/de")], | ||||||
|     ["en", /^en([_-]|$)/i,   () => msg("English"), () => Promise.resolve(DefaultLocaleModule)], |     ["en",      /^en([_-]|$)/i,      () => msg("English"),               async () => await import("@goauthentik/locales/en")], | ||||||
|     ["de", /^de([_-]|$)/i,   () => msg("German"),  () => import("@goauthentik/locales/de")], |     ["es",      /^es([_-]|$)/i,      () => msg("Spanish"),               async () => await import("@goauthentik/locales/es")], | ||||||
|     ["es", /^es([_-]|$)/i,   () => msg("Spanish"), () => import("@goauthentik/locales/es")], |     ["fr",      /^fr([_-]|$)/i,      () => msg("French"),                async () => await import("@goauthentik/locales/fr")], | ||||||
|     ["fr", /^fr([_-]|$)/i,   () => msg("French"),  () => import("@goauthentik/locales/fr")], |     ["it",      /^it([_-]|$)/i,      () => msg("Italian"),               async () => await import("@goauthentik/locales/it")], | ||||||
|     ["it", /^it([_-]|$)/i,   () => msg("Italian"), () => import("@goauthentik/locales/it")], |     ["ko",      /^ko([_-]|$)/i,      () => msg("Korean"),                async () => await import("@goauthentik/locales/ko")], | ||||||
|     ["ko", /^ko([_-]|$)/i,   () => msg("Korean"),  () => import("@goauthentik/locales/ko")], |     ["nl",      /^nl([_-]|$)/i,      () => msg("Dutch"),                 async () => await import("@goauthentik/locales/nl")], | ||||||
|     ["nl", /^nl([_-]|$)/i,   () => msg("Dutch"),   () => import("@goauthentik/locales/nl")], |     ["pl",      /^pl([_-]|$)/i,      () => msg("Polish"),                async () => await import("@goauthentik/locales/pl")], | ||||||
|     ["pl", /^pl([_-]|$)/i,   () => msg("Polish"),  () => import("@goauthentik/locales/pl")], |     ["ru",      /^ru([_-]|$)/i,      () => msg("Russian"),               async () => await import("@goauthentik/locales/ru")], | ||||||
|     ["ru", /^ru([_-]|$)/i,   () => msg("Russian"), () => import("@goauthentik/locales/ru")], |     ["tr",      /^tr([_-]|$)/i,      () => msg("Turkish"),               async () => await import("@goauthentik/locales/tr")], | ||||||
|     ["tr", /^tr([_-]|$)/i,   () => msg("Turkish"), () => import("@goauthentik/locales/tr")], |     ["zh_TW",   /^zh[_-]TW$/i,       () => msg("Taiwanese Mandarin"),    async () => await import("@goauthentik/locales/zh_TW")], | ||||||
|     ["zh_TW", /^zh[_-]TW$/i, () => msg("Taiwanese Mandarin"), () => import("@goauthentik/locales/zh_TW")], |     ["zh-Hans", /^zh(\b|_)/i,        () => msg("Chinese (simplified)"),  async () => await import("@goauthentik/locales/zh-Hans")], | ||||||
|     ["zh-Hans", /^zh(\b|_)/i, () => msg("Chinese (simplified)"), () => import("@goauthentik/locales/zh-Hans")], |     ["zh-Hant", /^zh[_-](HK|Hant)/i, () => msg("Chinese (traditional)"), async () => await import("@goauthentik/locales/zh-Hant")], | ||||||
|     ["zh-Hant", /^zh[_-](HK|Hant)/i, () => msg("Chinese (traditional)"), () => import("@goauthentik/locales/zh-Hant")], |     debug | ||||||
|     debug, |  | ||||||
| ]; | ]; | ||||||
|  |  | ||||||
| /** | export const LOCALES: AkLocale[] = LOCALE_TABLE.map(([code, match, label, locale]) => ({ | ||||||
|  * Available locales, identified by their ISO 639-1 language code. |     code, | ||||||
|  */ |     match, | ||||||
| export const AKLocalDefinitions: readonly AKLocaleDefinition[] = LOCALE_TABLE.map( |     label, | ||||||
|     ([languageCode, pattern, formatLabel, fetch]) => { |     locale, | ||||||
|         return { | })); | ||||||
|             languageCode, |  | ||||||
|             pattern, |  | ||||||
|             formatLabel, |  | ||||||
|             fetch, |  | ||||||
|         }; |  | ||||||
|     }, |  | ||||||
| ); |  | ||||||
|  |  | ||||||
| export default AKLocalDefinitions; | export default LOCALES; | ||||||
|  | |||||||
| @ -1,6 +0,0 @@ | |||||||
| export const EVENT_LOCALE_REQUEST = "ak-locale-request"; |  | ||||||
| export const EVENT_LOCALE_CHANGE = "ak-locale-change"; |  | ||||||
|  |  | ||||||
| export interface LocaleContextEventDetail { |  | ||||||
|     locale: string; |  | ||||||
| } |  | ||||||
| @ -1,80 +1,59 @@ | |||||||
| import { globalAK } from "@goauthentik/common/global"; | import { globalAK } from "@goauthentik/common/global"; | ||||||
|  |  | ||||||
| import { AKLocalDefinitions } from "./definitions.js"; | import { LOCALES as RAW_LOCALES, enLocale } from "./definitions"; | ||||||
| import { AKLocaleDefinition } from "./types.js"; | import { AkLocale } from "./types"; | ||||||
|  |  | ||||||
| export const DEFAULT_LOCALE = "en"; | export const DEFAULT_LOCALE = "en"; | ||||||
|  |  | ||||||
| export const EVENT_REQUEST_LOCALE = "ak-request-locale"; | export const EVENT_REQUEST_LOCALE = "ak-request-locale"; | ||||||
|  |  | ||||||
| /** | const TOMBSTONE = "⛼⛼tombstone⛼⛼"; | ||||||
|  * Find the locale definition for a given language code. |  | ||||||
|  */ |  | ||||||
| export function findLocaleDefinition(languageCode: string): AKLocaleDefinition | null { |  | ||||||
|     for (const locale of AKLocalDefinitions) { |  | ||||||
|         if (locale.pattern.test(languageCode)) { |  | ||||||
|             return locale; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return null; | // NOTE: This is the definition of the LOCALES table that most of the code uses. The 'definitions' | ||||||
|  | // file is relatively pure, but here we establish that we want the English locale to loaded when an | ||||||
|  | // application is first instantiated. | ||||||
|  |  | ||||||
|  | export const LOCALES = RAW_LOCALES.map((locale) => | ||||||
|  |     locale.code === "en" ? { ...locale, locale: async () => enLocale } : locale, | ||||||
|  | ); | ||||||
|  |  | ||||||
|  | export function getBestMatchLocale(locale: string): AkLocale | undefined { | ||||||
|  |     return LOCALES.find((l) => l.match.test(locale)); | ||||||
| } | } | ||||||
|  |  | ||||||
| // This looks weird, but it's sensible: we have several candidates, and we want to find the first | // This looks weird, but it's sensible: we have several candidates, and we want to find the first | ||||||
| // one that has a supported locale. Then, from *that*, we have to extract that first supported | // one that has a supported locale. Then, from *that*, we have to extract that first supported | ||||||
| // locale. | // locale. | ||||||
|  |  | ||||||
| export function findSupportedLocale(candidates: string[]): AKLocaleDefinition | null { | export function findSupportedLocale(candidates: string[]) { | ||||||
|     for (const candidate of candidates) { |     const candidate = candidates.find((candidate: string) => getBestMatchLocale(candidate)); | ||||||
|         const locale = findLocaleDefinition(candidate); |     return candidate ? getBestMatchLocale(candidate) : undefined; | ||||||
|  |  | ||||||
|         if (locale) return locale; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return null; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| export function localeCodeFromURL(param = "locale") { | export function localeCodeFromUrl(param = "locale") { | ||||||
|     const searchParams = new URLSearchParams(window.location.search); |     const url = new URL(window.location.href); | ||||||
|  |     return url.searchParams.get(param) || ""; | ||||||
|     return searchParams.get(param); |  | ||||||
| } | } | ||||||
|  |  | ||||||
| function isLocaleCodeCandidate(input: unknown): input is string { | // Get all locales we can, in order | ||||||
|     if (typeof input !== "string") return false; | // - Global authentik settings (contains user settings) | ||||||
|  | // - URL parameter | ||||||
|  | // - A requested code passed in, if any | ||||||
|  | // - Navigator | ||||||
|  | // - Fallback (en) | ||||||
|  |  | ||||||
|     return !!input; | const isLocaleCandidate = (v: unknown): v is string => | ||||||
| } |     typeof v === "string" && v !== "" && v !== TOMBSTONE; | ||||||
|  |  | ||||||
| /** | export function autoDetectLanguage(userReq = TOMBSTONE, brandReq = TOMBSTONE): string { | ||||||
|  * Auto-detect the most appropriate locale. |     const localeCandidates: string[] = [ | ||||||
|  * |         localeCodeFromUrl("locale"), | ||||||
|  * @remarks |         userReq, | ||||||
|  * |         window.navigator?.language ?? TOMBSTONE, | ||||||
|  * The order of precedence is: |         brandReq, | ||||||
|  * |         globalAK()?.locale ?? TOMBSTONE, | ||||||
|  * 1. URL parameter `locale`. |         DEFAULT_LOCALE, | ||||||
|  * 2. User's preferred locale, if any. |     ].filter(isLocaleCandidate); | ||||||
|  * 3. Browser's preferred locale, if any. |  | ||||||
|  * 4. Brand's preferred locale, if any. |  | ||||||
|  * 5. Default locale. |  | ||||||
|  * |  | ||||||
|  * @param requestedLanguageCode - The user's preferred locale, if any. |  | ||||||
|  * @param brandLanguageCode - The brand's preferred locale, if any. |  | ||||||
|  * |  | ||||||
|  * @returns The most appropriate locale. |  | ||||||
|  */ |  | ||||||
| export function autoDetectLanguage( |  | ||||||
|     requestedLanguageCode?: string, |  | ||||||
|     brandLanguageCode?: string, |  | ||||||
| ): string { |  | ||||||
|     const localeCandidates = [ |  | ||||||
|         localeCodeFromURL("locale"), |  | ||||||
|         requestedLanguageCode, |  | ||||||
|         window.navigator?.language, |  | ||||||
|         brandLanguageCode, |  | ||||||
|         globalAK()?.locale, |  | ||||||
|     ].filter(isLocaleCodeCandidate); |  | ||||||
|  |  | ||||||
|     const firstSupportedLocale = findSupportedLocale(localeCandidates); |     const firstSupportedLocale = findSupportedLocale(localeCandidates); | ||||||
|  |  | ||||||
| @ -82,11 +61,10 @@ export function autoDetectLanguage( | |||||||
|         console.debug( |         console.debug( | ||||||
|             `authentik/locale: No locale found for '[${localeCandidates}.join(',')]', falling back to ${DEFAULT_LOCALE}`, |             `authentik/locale: No locale found for '[${localeCandidates}.join(',')]', falling back to ${DEFAULT_LOCALE}`, | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         return DEFAULT_LOCALE; |         return DEFAULT_LOCALE; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     return firstSupportedLocale.languageCode; |     return firstSupportedLocale.code; | ||||||
| } | } | ||||||
|  |  | ||||||
| export default autoDetectLanguage; | export default autoDetectLanguage; | ||||||
|  | |||||||
| @ -1,21 +1,10 @@ | |||||||
| import type { LocaleModule } from "@lit/localize"; | import type { LocaleModule } from "@lit/localize"; | ||||||
|  |  | ||||||
| /** | export type LocaleRow = [string, RegExp, () => string, () => Promise<LocaleModule>]; | ||||||
|  * - ISO 639-1 code for the locale. |  | ||||||
|  * - Pattern to match the user-supplied locale. |  | ||||||
|  * - Human-readable label for the locale. |  | ||||||
|  * - Locale loader. |  | ||||||
|  */ |  | ||||||
| export type LocaleRow = [ |  | ||||||
|     languageCode: string, |  | ||||||
|     pattern: RegExp, |  | ||||||
|     formatLabel: () => string, |  | ||||||
|     fetch: () => Promise<LocaleModule>, |  | ||||||
| ]; |  | ||||||
|  |  | ||||||
| export interface AKLocaleDefinition { | export type AkLocale = { | ||||||
|     languageCode: string; |     code: string; | ||||||
|     pattern: RegExp; |     match: RegExp; | ||||||
|     formatLabel(): string; |     label: () => string; | ||||||
|     fetch(): Promise<LocaleModule>; |     locale: () => Promise<LocaleModule>; | ||||||
| } | }; | ||||||
|  | |||||||
							
								
								
									
										55
									
								
								web/src/elements/utils/iframe.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								web/src/elements/utils/iframe.ts
									
									
									
									
									
										Normal 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>`; | ||||||
|  | } | ||||||
| @ -1,4 +1,4 @@ | |||||||
| import { purify } from "@goauthentik/common/purify"; | import { BrandedHTMLPolicy, sanitizeHTML } from "@goauthentik/common/purify"; | ||||||
| import { AKElement } from "@goauthentik/elements/Base.js"; | import { AKElement } from "@goauthentik/elements/Base.js"; | ||||||
|  |  | ||||||
| import { msg } from "@lit/localize"; | import { msg } from "@lit/localize"; | ||||||
| @ -21,8 +21,6 @@ const styles = css` | |||||||
|     } |     } | ||||||
| `; | `; | ||||||
|  |  | ||||||
| const poweredBy: FooterLink = { name: msg("Powered by authentik"), href: null }; |  | ||||||
|  |  | ||||||
| @customElement("ak-brand-links") | @customElement("ak-brand-links") | ||||||
| export class BrandLinks extends AKElement { | export class BrandLinks extends AKElement { | ||||||
|     static get styles() { |     static get styles() { | ||||||
| @ -33,13 +31,21 @@ export class BrandLinks extends AKElement { | |||||||
|     links: FooterLink[] = []; |     links: FooterLink[] = []; | ||||||
|  |  | ||||||
|     render() { |     render() { | ||||||
|         const links = [...(this.links ?? []), poweredBy]; |         const links = [...(this.links ?? [])]; | ||||||
|  |  | ||||||
|         return html` <ul class="pf-c-list pf-m-inline"> |         return html` <ul class="pf-c-list pf-m-inline"> | ||||||
|             ${map(links, (link) => |             ${map(links, (link) => { | ||||||
|                 link.href |                 const children = sanitizeHTML(BrandedHTMLPolicy, link.name); | ||||||
|                     ? purify(html`<li><a href="${link.href}">${link.name}</a></li>`) |  | ||||||
|                     : html`<li><span>${link.name}</span></li>`, |                 if (link.href) { | ||||||
|             )} |                     return html`<li><a href="${link.href}">${children}</a></li>`; | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 return html`<li> | ||||||
|  |                     <span> ${children} </span> | ||||||
|  |                 </li>`; | ||||||
|  |             })} | ||||||
|  |             <li><span>${msg("Powered by authentik")}</span></li> | ||||||
|         </ul>`; |         </ul>`; | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,15 +1,16 @@ | |||||||
| ///<reference types="@hcaptcha/types"/> | /// <reference types="@hcaptcha/types"/> | ||||||
| import { renderStatic } from "@goauthentik/common/purify"; | /// <reference types="turnstile-types"/> | ||||||
|  | import { renderStaticHTMLUnsafe } from "@goauthentik/common/purify"; | ||||||
| import "@goauthentik/elements/EmptyState"; | import "@goauthentik/elements/EmptyState"; | ||||||
| import { akEmptyState } from "@goauthentik/elements/EmptyState"; | import { akEmptyState } from "@goauthentik/elements/EmptyState"; | ||||||
| import { bound } from "@goauthentik/elements/decorators/bound"; | import { bound } from "@goauthentik/elements/decorators/bound"; | ||||||
| import "@goauthentik/elements/forms/FormElement"; | import "@goauthentik/elements/forms/FormElement"; | ||||||
|  | import { createIFrameHTMLWrapper } from "@goauthentik/elements/utils/iframe"; | ||||||
| import { ListenerController } from "@goauthentik/elements/utils/listenerController.js"; | import { ListenerController } from "@goauthentik/elements/utils/listenerController.js"; | ||||||
| import { randomId } from "@goauthentik/elements/utils/randomId"; | import { randomId } from "@goauthentik/elements/utils/randomId"; | ||||||
| import "@goauthentik/flow/FormStatic"; | import "@goauthentik/flow/FormStatic"; | ||||||
| import { BaseStage } from "@goauthentik/flow/stages/base"; | import { BaseStage } from "@goauthentik/flow/stages/base"; | ||||||
| import { P, match } from "ts-pattern"; | import { P, match } from "ts-pattern"; | ||||||
| import type * as _ from "turnstile-types"; |  | ||||||
|  |  | ||||||
| import { msg } from "@lit/localize"; | import { msg } from "@lit/localize"; | ||||||
| import { CSSResult, PropertyValues, TemplateResult, css, html, nothing } from "lit"; | import { CSSResult, PropertyValues, TemplateResult, css, html, nothing } from "lit"; | ||||||
| @ -56,40 +57,36 @@ type CaptchaHandler = { | |||||||
| // a resize. Because the Captcha is itself in an iframe, the reported height is often off by some | // a resize. Because the Captcha is itself in an iframe, the reported height is often off by some | ||||||
| // margin, so adding 2rem of height to our container adds padding and prevents scroll bars or hidden | // margin, so adding 2rem of height to our container adds padding and prevents scroll bars or hidden | ||||||
| // rendering. | // rendering. | ||||||
|  | function iframeTemplate(children: TemplateResult, challengeURL: string): TemplateResult { | ||||||
|  |     return html` ${children} | ||||||
|  |         <script> | ||||||
|  |             new ResizeObserver((entries) => { | ||||||
|  |                 const height = | ||||||
|  |                     document.body.offsetHeight + | ||||||
|  |                     parseFloat(getComputedStyle(document.body).fontSize) * 2; | ||||||
|  |  | ||||||
| const iframeTemplate = (captchaElement: TemplateResult, challengeUrl: string) => |                 window.parent.postMessage({ | ||||||
|     html`<!doctype html> |                     message: "resize", | ||||||
|         <head> |                     source: "goauthentik.io", | ||||||
|             <html> |                     context: "flow-executor", | ||||||
|                 <body style="display:flex;flex-direction:row;justify-content:center;"> |                     size: { height }, | ||||||
|                     ${captchaElement} |                 }); | ||||||
|                     <script> |             }).observe(document.querySelector(".ak-captcha-container")); | ||||||
|                         new ResizeObserver((entries) => { |         </script> | ||||||
|                             const height = |  | ||||||
|                                 document.body.offsetHeight + |         <script src=${challengeURL}></script> | ||||||
|                                 parseFloat(getComputedStyle(document.body).fontSize) * 2; |  | ||||||
|                             window.parent.postMessage({ |         <script> | ||||||
|                                 message: "resize", |             function callback(token) { | ||||||
|                                 source: "goauthentik.io", |                 window.parent.postMessage({ | ||||||
|                                 context: "flow-executor", |                     message: "captcha", | ||||||
|                                 size: { height }, |                     source: "goauthentik.io", | ||||||
|                             }); |                     context: "flow-executor", | ||||||
|                         }).observe(document.querySelector(".ak-captcha-container")); |                     token, | ||||||
|                     </script> |                 }); | ||||||
|                     <script src=${challengeUrl}></script> |             } | ||||||
|                     <script> |         </script>`; | ||||||
|                         function callback(token) { | } | ||||||
|                             window.parent.postMessage({ |  | ||||||
|                                 message: "captcha", |  | ||||||
|                                 source: "goauthentik.io", |  | ||||||
|                                 context: "flow-executor", |  | ||||||
|                                 token: token, |  | ||||||
|                             }); |  | ||||||
|                         } |  | ||||||
|                     </script> |  | ||||||
|                 </body> |  | ||||||
|             </html> |  | ||||||
|         </head>`; |  | ||||||
|  |  | ||||||
| @customElement("ak-stage-captcha") | @customElement("ak-stage-captcha") | ||||||
| export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeResponseRequest> { | export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeResponseRequest> { | ||||||
| @ -305,11 +302,25 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     async renderFrame(captchaElement: TemplateResult) { |     async renderFrame(captchaElement: TemplateResult) { | ||||||
|         this.captchaFrame.contentWindow?.document.open(); |         const { contentDocument } = this.captchaFrame || {}; | ||||||
|         this.captchaFrame.contentWindow?.document.write( |  | ||||||
|             await renderStatic(iframeTemplate(captchaElement, this.challenge.jsUrl)), |         if (!contentDocument) { | ||||||
|  |             console.debug( | ||||||
|  |                 "authentik/stages/captcha: unable to render captcha frame, no contentDocument", | ||||||
|  |             ); | ||||||
|  |  | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         contentDocument.open(); | ||||||
|  |  | ||||||
|  |         contentDocument.write( | ||||||
|  |             createIFrameHTMLWrapper( | ||||||
|  |                 renderStaticHTMLUnsafe(iframeTemplate(captchaElement, this.challenge.jsUrl)), | ||||||
|  |             ), | ||||||
|         ); |         ); | ||||||
|         this.captchaFrame.contentWindow?.document.close(); |  | ||||||
|  |         contentDocument.close(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     renderBody() { |     renderBody() { | ||||||
|  | |||||||
| @ -4,7 +4,7 @@ import { | |||||||
|     CapabilitiesEnum, |     CapabilitiesEnum, | ||||||
|     WithCapabilitiesConfig, |     WithCapabilitiesConfig, | ||||||
| } from "@goauthentik/elements/Interface/capabilitiesProvider"; | } from "@goauthentik/elements/Interface/capabilitiesProvider"; | ||||||
| import { AKLocalDefinitions } from "@goauthentik/elements/ak-locale-context/definitions"; | import { LOCALES } from "@goauthentik/elements/ak-locale-context/definitions"; | ||||||
| import "@goauthentik/elements/forms/FormElement"; | import "@goauthentik/elements/forms/FormElement"; | ||||||
| import { BaseStage } from "@goauthentik/flow/stages/base"; | import { BaseStage } from "@goauthentik/flow/stages/base"; | ||||||
|  |  | ||||||
| @ -199,15 +199,15 @@ ${prompt.initialValue}</textarea | |||||||
|                 })}`; |                 })}`; | ||||||
|             case PromptTypeEnum.AkLocale: { |             case PromptTypeEnum.AkLocale: { | ||||||
|                 const locales = this.can(CapabilitiesEnum.CanDebug) |                 const locales = this.can(CapabilitiesEnum.CanDebug) | ||||||
|                     ? AKLocalDefinitions |                     ? LOCALES | ||||||
|                     : AKLocalDefinitions.filter((locale) => locale.languageCode !== "debug"); |                     : LOCALES.filter((locale) => locale.code !== "debug"); | ||||||
|                 const options = locales.map( |                 const options = locales.map( | ||||||
|                     (locale) => |                     (locale) => | ||||||
|                         html`<option |                         html`<option | ||||||
|                             value=${locale.languageCode} |                             value=${locale.code} | ||||||
|                             ?selected=${locale.languageCode === prompt.initialValue} |                             ?selected=${locale.code === prompt.initialValue} | ||||||
|                         > |                         > | ||||||
|                             ${locale.languageCode.toUpperCase()} - ${locale.formatLabel()} |                             ${locale.code.toUpperCase()} - ${locale.label()} | ||||||
|                         </option> `, |                         </option> `, | ||||||
|                 ); |                 ); | ||||||
|  |  | ||||||
|  | |||||||
| @ -146,6 +146,7 @@ When writing out steps in a procedural topic, avoid starting with "Once...". Ins | |||||||
|  |  | ||||||
| - Use _italic_ for: | - Use _italic_ for: | ||||||
|  |  | ||||||
|  |     - Variables or placeholders to indicate that the value should be replaced by the user (e.g., _your-domain.com_). Clearly indicate whether variables in code snippets need to be defined by the user, are system-provided, or generated. | ||||||
|     - Emphasis, but sparingly, to avoid overuse. For example, you can use italics for important terms or concepts on first mention in a section. |     - Emphasis, but sparingly, to avoid overuse. For example, you can use italics for important terms or concepts on first mention in a section. | ||||||
|  |  | ||||||
| - Use `code formatting` for: | - Use `code formatting` for: | ||||||
| @ -156,9 +157,11 @@ When writing out steps in a procedural topic, avoid starting with "Once...". Ins | |||||||
|  |  | ||||||
| - When handling URLs: | - When handling URLs: | ||||||
|  |  | ||||||
|     - For URLs entered as values or defined in fields, enclose any variables inside angle brackets (`< >`) to clearly indicate that these are placeholders that require user input. |     - For URLs entered as values or defined in fields _italicize_ any variables within them to emphasize that placeholders require user input. | ||||||
|  |  | ||||||
|         For example: `https://authentik.company/application/o/<slug>/.well-known/openid-configuration` |         In Markdown, use this syntax: `<kbd>https://<em>company-domain</em>/source/oauth/callback/<em>source-slug</em></kbd>` | ||||||
|  |  | ||||||
|  |         Rendered formatting: <kbd>https://<em>company-domain</em>/source/oauth/callback/<em>source-slug</em></kbd> | ||||||
|  |  | ||||||
|     - When mentioning URLs in text or within procedural instructions, omit code formatting. For instance: "In your browser, go to https://example.com." |     - When mentioning URLs in text or within procedural instructions, omit code formatting. For instance: "In your browser, go to https://example.com." | ||||||
|  |  | ||||||
|  | |||||||
| @ -7,43 +7,41 @@ title: User properties and attributes | |||||||
| The User object has the following properties: | The User object has the following properties: | ||||||
|  |  | ||||||
| - `username`: User's username. | - `username`: User's username. | ||||||
| - `email`: User's email. | - `email` User's email. | ||||||
| - `uid`: User's unique ID. Read-only. | - `uid` User's unique ID | ||||||
| - `name`: User's display name. | - `name` User's display name. | ||||||
| - `is_staff`: Boolean field defining if user is staff. | - `is_staff` Boolean field if user is staff. | ||||||
| - `is_active`: Boolean field defining if user is active. | - `is_active` Boolean field if user is active. | ||||||
| - `date_joined`: Date user joined/was created. Read-only. | - `date_joined` Date user joined/was created. | ||||||
| - `password_change_date`: Date password was last changed. Read-only. | - `password_change_date` Date password was last changed. | ||||||
| - `path`: User's path, see [Path](#path) | - `path` User's path, see [Path](#path) | ||||||
| - `attributes`: Dynamic attributes, see [Attributes](#attributes) | - `attributes` Dynamic attributes, see [Attributes](#attributes) | ||||||
| - `group_attributes()`: Merged attributes of all groups the user is member of and the user's own attributes. Ready-only. | - `group_attributes()` Merged attributes of all groups the user is member of and the user's own attributes. | ||||||
| - `ak_groups`: This is a queryset of all the user's groups. | - `ak_groups` This is a queryset of all the user's groups. | ||||||
|  |  | ||||||
|  |     You can do additional filtering like: | ||||||
|  |  | ||||||
|  |     ```python | ||||||
|  |     user.ak_groups.filter(name__startswith='test') | ||||||
|  |     ``` | ||||||
|  |  | ||||||
|  |     For Django field lookups, see [here](https://docs.djangoproject.com/en/4.2/ref/models/querysets/#id4). | ||||||
|  |  | ||||||
|  |     To get the name of all groups, you can use this command: | ||||||
|  |  | ||||||
|  |     ```python | ||||||
|  |     [group.name for group in user.ak_groups.all()] | ||||||
|  |     ``` | ||||||
|  |  | ||||||
| ## Examples | ## Examples | ||||||
|  |  | ||||||
| These are examples of how User objects can be used within Policies and Property Mappings. | List all the User's group names: | ||||||
|  |  | ||||||
| ### List a user's group memberships |  | ||||||
|  |  | ||||||
| Use the following example to list all groups that a User object is a member of: |  | ||||||
|  |  | ||||||
| ```python | ```python | ||||||
| for group in user.ak_groups.all(): | for group in user.ak_groups.all(): | ||||||
|     yield group.name |     yield group.name | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| ### List a user's group memberships and filter based on group name |  | ||||||
|  |  | ||||||
| Use the following example to list groups that a User object is a member of, but filter based on group name: |  | ||||||
|  |  | ||||||
| ```python |  | ||||||
| user.ak_groups.filter(name__startswith='test') |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| :::info |  | ||||||
| For Django field lookups, see the [Django documentation](https://docs.djangoproject.com/en/stable/ref/models/querysets/#id4). |  | ||||||
| ::: |  | ||||||
|  |  | ||||||
| ## Path | ## Path | ||||||
|  |  | ||||||
| Paths can be used to organize users into folders depending on which source created them or organizational structure. Paths may not start or end with a slash, but they can contain any other character as path segments. The paths are currently purely used for organization, it does not affect their permissions, group memberships, or anything else. | Paths can be used to organize users into folders depending on which source created them or organizational structure. Paths may not start or end with a slash, but they can contain any other character as path segments. The paths are currently purely used for organization, it does not affect their permissions, group memberships, or anything else. | ||||||
| @ -89,7 +87,7 @@ This field is only used by the Proxy Provider. | |||||||
| Some applications can be configured to create new users using header information forwarded from authentik. You can forward additional header information by adding each header | Some applications can be configured to create new users using header information forwarded from authentik. You can forward additional header information by adding each header | ||||||
| underneath `additionalHeaders`: | underneath `additionalHeaders`: | ||||||
|  |  | ||||||
| #### Example | #### Example: | ||||||
|  |  | ||||||
| ```yaml | ```yaml | ||||||
| additionalHeaders: | additionalHeaders: | ||||||
|  | |||||||
| @ -66,7 +66,7 @@ environment: | |||||||
|                 "client_id": "<Client ID>", |                 "client_id": "<Client ID>", | ||||||
|                 "secret": "<Client Secret>", |                 "secret": "<Client Secret>", | ||||||
|                 "settings": { |                 "settings": { | ||||||
|                   "server_url": "https://authentik.company/application/o/<slug>/.well-known/openid-configuration" |                   "server_url": "https://authentik.company/application/o/paperless/.well-known/openid-configuration" | ||||||
|                 } |                 } | ||||||
|               } |               } | ||||||
|             ], |             ], | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user
	