Compare commits
	
		
			12 Commits
		
	
	
		
			safari-cra
			...
			dependabot
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| d0f88f5214 | |||
| 2bef7695db | |||
| df472dd842 | |||
| 98d201d34c | |||
| 47e89602ab | |||
| ceb0851452 | |||
| cac2593658 | |||
| 1c9705bfaa | |||
| 9e2566cec4 | |||
| 5bdef1c4f6 | |||
| ae41ccd862 | |||
| 337956672f | 
							
								
								
									
										8
									
								
								.github/workflows/packages-npm-publish.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/workflows/packages-npm-publish.yml
									
									
									
									
										vendored
									
									
								
							@ -3,10 +3,10 @@ on:
 | 
			
		||||
  push:
 | 
			
		||||
    branches: [main]
 | 
			
		||||
    paths:
 | 
			
		||||
      - packages/docusaurus-config
 | 
			
		||||
      - packages/eslint-config
 | 
			
		||||
      - packages/prettier-config
 | 
			
		||||
      - packages/tsconfig
 | 
			
		||||
      - packages/docusaurus-config/**
 | 
			
		||||
      - packages/eslint-config/**
 | 
			
		||||
      - packages/prettier-config/**
 | 
			
		||||
      - packages/tsconfig/**
 | 
			
		||||
  workflow_dispatch:
 | 
			
		||||
jobs:
 | 
			
		||||
  publish:
 | 
			
		||||
 | 
			
		||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@ -18,7 +18,7 @@ msgid ""
 | 
			
		||||
msgstr ""
 | 
			
		||||
"Project-Id-Version: PACKAGE VERSION\n"
 | 
			
		||||
"Report-Msgid-Bugs-To: \n"
 | 
			
		||||
"POT-Creation-Date: 2025-04-11 00:10+0000\n"
 | 
			
		||||
"POT-Creation-Date: 2025-04-23 09:00+0000\n"
 | 
			
		||||
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
 | 
			
		||||
"Last-Translator: Gil Poiares-Oliveira, 2025\n"
 | 
			
		||||
"Language-Team: Portuguese (Brazil) (https://app.transifex.com/authentik/teams/119923/pt_BR/)\n"
 | 
			
		||||
@ -192,6 +192,7 @@ msgid "User's display name."
 | 
			
		||||
msgstr "Nome de exibição do usuário."
 | 
			
		||||
 | 
			
		||||
#: authentik/core/models.py authentik/providers/oauth2/models.py
 | 
			
		||||
#: authentik/rbac/models.py
 | 
			
		||||
msgid "User"
 | 
			
		||||
msgstr "Usuário"
 | 
			
		||||
 | 
			
		||||
@ -376,6 +377,18 @@ msgstr "Mapeamento de propriedades"
 | 
			
		||||
msgid "Property Mappings"
 | 
			
		||||
msgstr "Mapeamentos de propriedades"
 | 
			
		||||
 | 
			
		||||
#: authentik/core/models.py
 | 
			
		||||
msgid "session data"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/core/models.py
 | 
			
		||||
msgid "Session"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/core/models.py
 | 
			
		||||
msgid "Sessions"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/core/models.py
 | 
			
		||||
msgid "Authenticated Session"
 | 
			
		||||
msgstr "Sessão Autenticada"
 | 
			
		||||
@ -483,6 +496,38 @@ msgstr "Uso de licença"
 | 
			
		||||
msgid "License Usage Records"
 | 
			
		||||
msgstr "Registros de uso de licença"
 | 
			
		||||
 | 
			
		||||
#: authentik/enterprise/policies/unique_password/models.py
 | 
			
		||||
#: authentik/policies/password/models.py
 | 
			
		||||
msgid "Field key to check, field keys defined in Prompt stages are available."
 | 
			
		||||
msgstr ""
 | 
			
		||||
"Chave de campo para verificar, as chaves de campo definidas nos estágios de "
 | 
			
		||||
"prompt estão disponíveis."
 | 
			
		||||
 | 
			
		||||
#: authentik/enterprise/policies/unique_password/models.py
 | 
			
		||||
msgid "Number of passwords to check against."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/enterprise/policies/unique_password/models.py
 | 
			
		||||
#: authentik/policies/password/models.py
 | 
			
		||||
msgid "Password not set in context"
 | 
			
		||||
msgstr "Senha não definida no contexto"
 | 
			
		||||
 | 
			
		||||
#: authentik/enterprise/policies/unique_password/models.py
 | 
			
		||||
msgid "This password has been used previously. Please choose a different one."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/enterprise/policies/unique_password/models.py
 | 
			
		||||
msgid "Password Uniqueness Policy"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/enterprise/policies/unique_password/models.py
 | 
			
		||||
msgid "Password Uniqueness Policies"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/enterprise/policies/unique_password/models.py
 | 
			
		||||
msgid "User Password History"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/enterprise/policy.py
 | 
			
		||||
msgid "Enterprise required to access this feature."
 | 
			
		||||
msgstr "Entrerprise é necessário para acessar essa funcionalidade"
 | 
			
		||||
@ -1252,12 +1297,6 @@ msgstr ""
 | 
			
		||||
msgid "Clear Policy's cache metrics"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/policies/password/models.py
 | 
			
		||||
msgid "Field key to check, field keys defined in Prompt stages are available."
 | 
			
		||||
msgstr ""
 | 
			
		||||
"Chave de campo para verificar, as chaves de campo definidas nos estágios de "
 | 
			
		||||
"prompt estão disponíveis."
 | 
			
		||||
 | 
			
		||||
#: authentik/policies/password/models.py
 | 
			
		||||
msgid "How many times the password hash is allowed to be on haveibeenpwned"
 | 
			
		||||
msgstr "Quantas vezes o hash da senha pode estar em haveibeenpwned"
 | 
			
		||||
@ -1268,10 +1307,6 @@ msgid ""
 | 
			
		||||
msgstr ""
 | 
			
		||||
"Se a pontuação zxcvbn for igual ou menor que esse valor, a política falhará."
 | 
			
		||||
 | 
			
		||||
#: authentik/policies/password/models.py
 | 
			
		||||
msgid "Password not set in context"
 | 
			
		||||
msgstr "Senha não definida no contexto"
 | 
			
		||||
 | 
			
		||||
#: authentik/policies/password/models.py
 | 
			
		||||
msgid "Invalid password."
 | 
			
		||||
msgstr ""
 | 
			
		||||
@ -1313,20 +1348,6 @@ msgstr "Pontuação de reputação"
 | 
			
		||||
msgid "Reputation Scores"
 | 
			
		||||
msgstr "Pontuações de reputação"
 | 
			
		||||
 | 
			
		||||
#: 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
 | 
			
		||||
msgid "Permission denied"
 | 
			
		||||
msgstr "Permissão negada"
 | 
			
		||||
@ -2141,6 +2162,10 @@ msgstr ""
 | 
			
		||||
msgid "Roles"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/rbac/models.py
 | 
			
		||||
msgid "Initial Permissions"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/rbac/models.py
 | 
			
		||||
msgid "System permission"
 | 
			
		||||
msgstr "Permissão do sistema"
 | 
			
		||||
@ -2387,6 +2412,22 @@ msgstr ""
 | 
			
		||||
msgid "LDAP Source Property Mappings"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/sources/ldap/models.py
 | 
			
		||||
msgid "User LDAP Source Connection"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/sources/ldap/models.py
 | 
			
		||||
msgid "User LDAP Source Connections"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/sources/ldap/models.py
 | 
			
		||||
msgid "Group LDAP Source Connection"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/sources/ldap/models.py
 | 
			
		||||
msgid "Group LDAP Source Connections"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/sources/ldap/signals.py
 | 
			
		||||
msgid "Password does not match Active Directory Complexity."
 | 
			
		||||
msgstr "A senha não corresponde à complexidade do Active Directory."
 | 
			
		||||
@ -2395,6 +2436,14 @@ msgstr "A senha não corresponde à complexidade do Active Directory."
 | 
			
		||||
msgid "No token received."
 | 
			
		||||
msgstr "Nenhum token recebido."
 | 
			
		||||
 | 
			
		||||
#: authentik/sources/oauth/models.py
 | 
			
		||||
msgid "HTTP Basic Authentication"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/sources/oauth/models.py
 | 
			
		||||
msgid "Include the client ID and secret as request parameters"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/sources/oauth/models.py
 | 
			
		||||
msgid "Request Token URL"
 | 
			
		||||
msgstr "URL do token de solicitação"
 | 
			
		||||
@ -2435,6 +2484,12 @@ msgstr "URL usado pelo authentik para obter informações do usuário."
 | 
			
		||||
msgid "Additional Scopes"
 | 
			
		||||
msgstr "Escopos Adicionais"
 | 
			
		||||
 | 
			
		||||
#: authentik/sources/oauth/models.py
 | 
			
		||||
msgid ""
 | 
			
		||||
"How to perform authentication during an authorization_code token request "
 | 
			
		||||
"flow"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/sources/oauth/models.py
 | 
			
		||||
msgid "OAuth Source"
 | 
			
		||||
msgstr "Fonte OAuth"
 | 
			
		||||
@ -3318,6 +3373,12 @@ msgid ""
 | 
			
		||||
"info is entered."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/stages/identification/models.py
 | 
			
		||||
msgid ""
 | 
			
		||||
"Show the user the 'Remember me on this device' toggle, allowing repeat users"
 | 
			
		||||
" to skip straight to entering their password."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/stages/identification/models.py
 | 
			
		||||
msgid "Optional enrollment flow, which is linked at the bottom of the page."
 | 
			
		||||
msgstr "Optional enrollment flow, which is linked at the bottom of the page."
 | 
			
		||||
@ -3678,6 +3739,14 @@ msgstr ""
 | 
			
		||||
"Os eventos serão excluídos após esta duração.(Formato: "
 | 
			
		||||
"semanas=3;dias=2;horas=3,segundos=2)."
 | 
			
		||||
 | 
			
		||||
#: authentik/tenants/models.py
 | 
			
		||||
msgid "Reputation cannot decrease lower than this value. Zero or negative."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/tenants/models.py
 | 
			
		||||
msgid "Reputation cannot increase higher than this value. Zero or positive."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/tenants/models.py
 | 
			
		||||
msgid "The option configures the footer links on the flow executor pages."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
@ -13,7 +13,7 @@ msgid ""
 | 
			
		||||
msgstr ""
 | 
			
		||||
"Project-Id-Version: PACKAGE VERSION\n"
 | 
			
		||||
"Report-Msgid-Bugs-To: \n"
 | 
			
		||||
"POT-Creation-Date: 2025-03-31 00:10+0000\n"
 | 
			
		||||
"POT-Creation-Date: 2025-04-23 09:00+0000\n"
 | 
			
		||||
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
 | 
			
		||||
"Last-Translator: Jens L. <jens@goauthentik.io>, 2025\n"
 | 
			
		||||
"Language-Team: Turkish (https://app.transifex.com/authentik/teams/119923/tr/)\n"
 | 
			
		||||
@ -187,6 +187,7 @@ msgid "User's display name."
 | 
			
		||||
msgstr "Kullanıcının görünen adı."
 | 
			
		||||
 | 
			
		||||
#: authentik/core/models.py authentik/providers/oauth2/models.py
 | 
			
		||||
#: authentik/rbac/models.py
 | 
			
		||||
msgid "User"
 | 
			
		||||
msgstr "Kullanıcı"
 | 
			
		||||
 | 
			
		||||
@ -372,6 +373,18 @@ msgstr "Özellik Eşleme"
 | 
			
		||||
msgid "Property Mappings"
 | 
			
		||||
msgstr "Özellik Eşlemeleri"
 | 
			
		||||
 | 
			
		||||
#: authentik/core/models.py
 | 
			
		||||
msgid "session data"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/core/models.py
 | 
			
		||||
msgid "Session"
 | 
			
		||||
msgstr "Oturum"
 | 
			
		||||
 | 
			
		||||
#: authentik/core/models.py
 | 
			
		||||
msgid "Sessions"
 | 
			
		||||
msgstr "Oturumlar"
 | 
			
		||||
 | 
			
		||||
#: authentik/core/models.py
 | 
			
		||||
msgid "Authenticated Session"
 | 
			
		||||
msgstr "Kimliği Doğrulanmış Oturum"
 | 
			
		||||
@ -479,6 +492,38 @@ msgstr "Lisans Kullanımı"
 | 
			
		||||
msgid "License Usage Records"
 | 
			
		||||
msgstr "Lisans Kullanım Kayıtları"
 | 
			
		||||
 | 
			
		||||
#: authentik/enterprise/policies/unique_password/models.py
 | 
			
		||||
#: authentik/policies/password/models.py
 | 
			
		||||
msgid "Field key to check, field keys defined in Prompt stages are available."
 | 
			
		||||
msgstr ""
 | 
			
		||||
"Alan tuşu kontrol etmek için, İstem aşamalarında tanımlanan alan tuşları "
 | 
			
		||||
"mevcuttur."
 | 
			
		||||
 | 
			
		||||
#: authentik/enterprise/policies/unique_password/models.py
 | 
			
		||||
msgid "Number of passwords to check against."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/enterprise/policies/unique_password/models.py
 | 
			
		||||
#: authentik/policies/password/models.py
 | 
			
		||||
msgid "Password not set in context"
 | 
			
		||||
msgstr "Parola bağlam içinde ayarlanmamış"
 | 
			
		||||
 | 
			
		||||
#: authentik/enterprise/policies/unique_password/models.py
 | 
			
		||||
msgid "This password has been used previously. Please choose a different one."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/enterprise/policies/unique_password/models.py
 | 
			
		||||
msgid "Password Uniqueness Policy"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/enterprise/policies/unique_password/models.py
 | 
			
		||||
msgid "Password Uniqueness Policies"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/enterprise/policies/unique_password/models.py
 | 
			
		||||
msgid "User Password History"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/enterprise/policy.py
 | 
			
		||||
msgid "Enterprise required to access this feature."
 | 
			
		||||
msgstr "Bu özelliğe erişmek için Kurumsal Paket gereklidir."
 | 
			
		||||
@ -1253,12 +1298,6 @@ msgstr "İlke'nin önbellek ölçümlerini görüntüleme"
 | 
			
		||||
msgid "Clear Policy's cache metrics"
 | 
			
		||||
msgstr "İlke'nin önbellek ölçümlerini temizleyin"
 | 
			
		||||
 | 
			
		||||
#: authentik/policies/password/models.py
 | 
			
		||||
msgid "Field key to check, field keys defined in Prompt stages are available."
 | 
			
		||||
msgstr ""
 | 
			
		||||
"Alan tuşu kontrol etmek için, İstem aşamalarında tanımlanan alan tuşları "
 | 
			
		||||
"mevcuttur."
 | 
			
		||||
 | 
			
		||||
#: authentik/policies/password/models.py
 | 
			
		||||
msgid "How many times the password hash is allowed to be on haveibeenpwned"
 | 
			
		||||
msgstr ""
 | 
			
		||||
@ -1271,10 +1310,6 @@ msgstr ""
 | 
			
		||||
"Eğer zxcvbn puanı bu değere eşit veya daha az ise, politika başarısız "
 | 
			
		||||
"olacaktır."
 | 
			
		||||
 | 
			
		||||
#: authentik/policies/password/models.py
 | 
			
		||||
msgid "Password not set in context"
 | 
			
		||||
msgstr "Parola bağlam içinde ayarlanmamış"
 | 
			
		||||
 | 
			
		||||
#: authentik/policies/password/models.py
 | 
			
		||||
msgid "Invalid password."
 | 
			
		||||
msgstr ""
 | 
			
		||||
@ -1316,20 +1351,6 @@ msgstr "İtibar Puanı"
 | 
			
		||||
msgid "Reputation Scores"
 | 
			
		||||
msgstr "İtibar Puanları"
 | 
			
		||||
 | 
			
		||||
#: 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
 | 
			
		||||
msgid "Permission denied"
 | 
			
		||||
msgstr "İzin reddedildi"
 | 
			
		||||
@ -2155,6 +2176,10 @@ msgstr "Rol"
 | 
			
		||||
msgid "Roles"
 | 
			
		||||
msgstr "Roller"
 | 
			
		||||
 | 
			
		||||
#: authentik/rbac/models.py
 | 
			
		||||
msgid "Initial Permissions"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/rbac/models.py
 | 
			
		||||
msgid "System permission"
 | 
			
		||||
msgstr "Sistem yetkisi"
 | 
			
		||||
@ -2398,6 +2423,13 @@ msgstr ""
 | 
			
		||||
"Bir kullanıcı parolasını değiştirdiğinde, parolayı LDAP ile geri eşitleyin. "
 | 
			
		||||
"Bu yalnızca tek bir LDAP kaynağında etkinleştirilebilir."
 | 
			
		||||
 | 
			
		||||
#: authentik/sources/ldap/models.py
 | 
			
		||||
msgid ""
 | 
			
		||||
"Lookup group membership based on a user attribute instead of a group "
 | 
			
		||||
"attribute. This allows nested group resolution on systems like FreeIPA and "
 | 
			
		||||
"Active Directory"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/sources/ldap/models.py
 | 
			
		||||
msgid "LDAP Source"
 | 
			
		||||
msgstr "LDAP Kaynağı"
 | 
			
		||||
@ -2414,6 +2446,22 @@ msgstr "LDAP Kaynak Özellik Eşlemesi"
 | 
			
		||||
msgid "LDAP Source Property Mappings"
 | 
			
		||||
msgstr "LDAP Kaynak Özellik Eşlemeleri"
 | 
			
		||||
 | 
			
		||||
#: authentik/sources/ldap/models.py
 | 
			
		||||
msgid "User LDAP Source Connection"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/sources/ldap/models.py
 | 
			
		||||
msgid "User LDAP Source Connections"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/sources/ldap/models.py
 | 
			
		||||
msgid "Group LDAP Source Connection"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/sources/ldap/models.py
 | 
			
		||||
msgid "Group LDAP Source Connections"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/sources/ldap/signals.py
 | 
			
		||||
msgid "Password does not match Active Directory Complexity."
 | 
			
		||||
msgstr "Parola Active Directory Karmaşıklığıyla eşleşmiyor."
 | 
			
		||||
@ -2422,6 +2470,14 @@ msgstr "Parola Active Directory Karmaşıklığıyla eşleşmiyor."
 | 
			
		||||
msgid "No token received."
 | 
			
		||||
msgstr "Jeton alınmadı."
 | 
			
		||||
 | 
			
		||||
#: authentik/sources/oauth/models.py
 | 
			
		||||
msgid "HTTP Basic Authentication"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/sources/oauth/models.py
 | 
			
		||||
msgid "Include the client ID and secret as request parameters"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/sources/oauth/models.py
 | 
			
		||||
msgid "Request Token URL"
 | 
			
		||||
msgstr "Jeton URL'si İste"
 | 
			
		||||
@ -2462,6 +2518,12 @@ msgstr "Kullanıcı bilgilerini almak için authentik tarafından kullanılan UR
 | 
			
		||||
msgid "Additional Scopes"
 | 
			
		||||
msgstr "Ek Kapsamlar"
 | 
			
		||||
 | 
			
		||||
#: authentik/sources/oauth/models.py
 | 
			
		||||
msgid ""
 | 
			
		||||
"How to perform authentication during an authorization_code token request "
 | 
			
		||||
"flow"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/sources/oauth/models.py
 | 
			
		||||
msgid "OAuth Source"
 | 
			
		||||
msgstr "OAuth Kaynağı"
 | 
			
		||||
@ -3360,6 +3422,12 @@ msgstr ""
 | 
			
		||||
"Etkinleştirildiğinde, yanlış kullanıcı bilgisi girilse bile aşama başarılı "
 | 
			
		||||
"olur ve devam eder."
 | 
			
		||||
 | 
			
		||||
#: authentik/stages/identification/models.py
 | 
			
		||||
msgid ""
 | 
			
		||||
"Show the user the 'Remember me on this device' toggle, allowing repeat users"
 | 
			
		||||
" to skip straight to entering their password."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/stages/identification/models.py
 | 
			
		||||
msgid "Optional enrollment flow, which is linked at the bottom of the page."
 | 
			
		||||
msgstr "Sayfanın alt kısmında bağlanan isteğe bağlı kayıt akışı."
 | 
			
		||||
@ -3734,6 +3802,14 @@ msgstr ""
 | 
			
		||||
"Olaylar bu süreden sonra silinecektir (Format: "
 | 
			
		||||
"weeks=3;days=2;hours=3,seconds=2)."
 | 
			
		||||
 | 
			
		||||
#: authentik/tenants/models.py
 | 
			
		||||
msgid "Reputation cannot decrease lower than this value. Zero or negative."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/tenants/models.py
 | 
			
		||||
msgid "Reputation cannot increase higher than this value. Zero or positive."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/tenants/models.py
 | 
			
		||||
msgid "The option configures the footer links on the flow executor pages."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
@ -14,7 +14,7 @@ msgid ""
 | 
			
		||||
msgstr ""
 | 
			
		||||
"Project-Id-Version: PACKAGE VERSION\n"
 | 
			
		||||
"Report-Msgid-Bugs-To: \n"
 | 
			
		||||
"POT-Creation-Date: 2025-04-11 00:10+0000\n"
 | 
			
		||||
"POT-Creation-Date: 2025-04-23 09:00+0000\n"
 | 
			
		||||
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
 | 
			
		||||
"Last-Translator: 刘松, 2025\n"
 | 
			
		||||
"Language-Team: Chinese (Taiwan) (https://app.transifex.com/authentik/teams/119923/zh_TW/)\n"
 | 
			
		||||
@ -178,6 +178,7 @@ msgid "User's display name."
 | 
			
		||||
msgstr "使用者的顯示名稱。"
 | 
			
		||||
 | 
			
		||||
#: authentik/core/models.py authentik/providers/oauth2/models.py
 | 
			
		||||
#: authentik/rbac/models.py
 | 
			
		||||
msgid "User"
 | 
			
		||||
msgstr "使用者"
 | 
			
		||||
 | 
			
		||||
@ -344,6 +345,18 @@ msgstr "屬性對應"
 | 
			
		||||
msgid "Property Mappings"
 | 
			
		||||
msgstr "屬性對應"
 | 
			
		||||
 | 
			
		||||
#: authentik/core/models.py
 | 
			
		||||
msgid "session data"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/core/models.py
 | 
			
		||||
msgid "Session"
 | 
			
		||||
msgstr "会话"
 | 
			
		||||
 | 
			
		||||
#: authentik/core/models.py
 | 
			
		||||
msgid "Sessions"
 | 
			
		||||
msgstr "会话"
 | 
			
		||||
 | 
			
		||||
#: authentik/core/models.py
 | 
			
		||||
msgid "Authenticated Session"
 | 
			
		||||
msgstr "已認證會談"
 | 
			
		||||
@ -447,6 +460,36 @@ msgstr "授權使用情況"
 | 
			
		||||
msgid "License Usage Records"
 | 
			
		||||
msgstr "授權使用紀錄"
 | 
			
		||||
 | 
			
		||||
#: authentik/enterprise/policies/unique_password/models.py
 | 
			
		||||
#: authentik/policies/password/models.py
 | 
			
		||||
msgid "Field key to check, field keys defined in Prompt stages are available."
 | 
			
		||||
msgstr "要檢查的欄位鍵,在提示階段中有可用的已定義欄位鍵。"
 | 
			
		||||
 | 
			
		||||
#: authentik/enterprise/policies/unique_password/models.py
 | 
			
		||||
msgid "Number of passwords to check against."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/enterprise/policies/unique_password/models.py
 | 
			
		||||
#: authentik/policies/password/models.py
 | 
			
		||||
msgid "Password not set in context"
 | 
			
		||||
msgstr "未在上下文中設定密碼"
 | 
			
		||||
 | 
			
		||||
#: authentik/enterprise/policies/unique_password/models.py
 | 
			
		||||
msgid "This password has been used previously. Please choose a different one."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/enterprise/policies/unique_password/models.py
 | 
			
		||||
msgid "Password Uniqueness Policy"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/enterprise/policies/unique_password/models.py
 | 
			
		||||
msgid "Password Uniqueness Policies"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/enterprise/policies/unique_password/models.py
 | 
			
		||||
msgid "User Password History"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/enterprise/policy.py
 | 
			
		||||
msgid "Enterprise required to access this feature."
 | 
			
		||||
msgstr "企業版才能存取此功能。"
 | 
			
		||||
@ -1176,10 +1219,6 @@ msgstr "檢視原則的快取指標"
 | 
			
		||||
msgid "Clear Policy's cache metrics"
 | 
			
		||||
msgstr "清除原則的快取指標"
 | 
			
		||||
 | 
			
		||||
#: authentik/policies/password/models.py
 | 
			
		||||
msgid "Field key to check, field keys defined in Prompt stages are available."
 | 
			
		||||
msgstr "要檢查的欄位鍵,在提示階段中有可用的已定義欄位鍵。"
 | 
			
		||||
 | 
			
		||||
#: authentik/policies/password/models.py
 | 
			
		||||
msgid "How many times the password hash is allowed to be on haveibeenpwned"
 | 
			
		||||
msgstr "密碼雜湊在 haveibeenpwned 上允許出現的次數"
 | 
			
		||||
@ -1189,10 +1228,6 @@ msgid ""
 | 
			
		||||
"If the zxcvbn score is equal or less than this value, the policy will fail."
 | 
			
		||||
msgstr "如果 zxcvbn 分數等於或小於此值,則該政策將失敗。"
 | 
			
		||||
 | 
			
		||||
#: authentik/policies/password/models.py
 | 
			
		||||
msgid "Password not set in context"
 | 
			
		||||
msgstr "未在上下文中設定密碼"
 | 
			
		||||
 | 
			
		||||
#: authentik/policies/password/models.py
 | 
			
		||||
msgid "Invalid password."
 | 
			
		||||
msgstr ""
 | 
			
		||||
@ -1234,20 +1269,6 @@ msgstr "信譽分數"
 | 
			
		||||
msgid "Reputation Scores"
 | 
			
		||||
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
 | 
			
		||||
msgid "Permission denied"
 | 
			
		||||
msgstr "權限不足。"
 | 
			
		||||
@ -1999,6 +2020,10 @@ msgstr "角色"
 | 
			
		||||
msgid "Roles"
 | 
			
		||||
msgstr "角色"
 | 
			
		||||
 | 
			
		||||
#: authentik/rbac/models.py
 | 
			
		||||
msgid "Initial Permissions"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/rbac/models.py
 | 
			
		||||
msgid "System permission"
 | 
			
		||||
msgstr "系統權限"
 | 
			
		||||
@ -2240,6 +2265,22 @@ msgstr ""
 | 
			
		||||
msgid "LDAP Source Property Mappings"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/sources/ldap/models.py
 | 
			
		||||
msgid "User LDAP Source Connection"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/sources/ldap/models.py
 | 
			
		||||
msgid "User LDAP Source Connections"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/sources/ldap/models.py
 | 
			
		||||
msgid "Group LDAP Source Connection"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/sources/ldap/models.py
 | 
			
		||||
msgid "Group LDAP Source Connections"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/sources/ldap/signals.py
 | 
			
		||||
msgid "Password does not match Active Directory Complexity."
 | 
			
		||||
msgstr "密碼不符合 Active Directory 的複雜性要求。"
 | 
			
		||||
@ -2248,6 +2289,14 @@ msgstr "密碼不符合 Active Directory 的複雜性要求。"
 | 
			
		||||
msgid "No token received."
 | 
			
		||||
msgstr "未收到權杖。"
 | 
			
		||||
 | 
			
		||||
#: authentik/sources/oauth/models.py
 | 
			
		||||
msgid "HTTP Basic Authentication"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/sources/oauth/models.py
 | 
			
		||||
msgid "Include the client ID and secret as request parameters"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/sources/oauth/models.py
 | 
			
		||||
msgid "Request Token URL"
 | 
			
		||||
msgstr "請求權杖的網址"
 | 
			
		||||
@ -2286,6 +2335,12 @@ msgstr "authentik 用來擷取使用者資訊的網址。"
 | 
			
		||||
msgid "Additional Scopes"
 | 
			
		||||
msgstr "附加範圍"
 | 
			
		||||
 | 
			
		||||
#: authentik/sources/oauth/models.py
 | 
			
		||||
msgid ""
 | 
			
		||||
"How to perform authentication during an authorization_code token request "
 | 
			
		||||
"flow"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/sources/oauth/models.py
 | 
			
		||||
msgid "OAuth Source"
 | 
			
		||||
msgstr "OAuth 來源"
 | 
			
		||||
@ -3137,6 +3192,12 @@ msgid ""
 | 
			
		||||
"info is entered."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/stages/identification/models.py
 | 
			
		||||
msgid ""
 | 
			
		||||
"Show the user the 'Remember me on this device' toggle, allowing repeat users"
 | 
			
		||||
" to skip straight to entering their password."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/stages/identification/models.py
 | 
			
		||||
msgid "Optional enrollment flow, which is linked at the bottom of the page."
 | 
			
		||||
msgstr "可選的註冊流程,連結在頁面的底部。"
 | 
			
		||||
@ -3481,6 +3542,14 @@ msgid ""
 | 
			
		||||
"weeks=3;days=2;hours=3,seconds=2)."
 | 
			
		||||
msgstr "事件將在此期間後刪除。(格式:weeks=3;days=2;hours=3,seconds=2)"
 | 
			
		||||
 | 
			
		||||
#: authentik/tenants/models.py
 | 
			
		||||
msgid "Reputation cannot decrease lower than this value. Zero or negative."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/tenants/models.py
 | 
			
		||||
msgid "Reputation cannot increase higher than this value. Zero or positive."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: authentik/tenants/models.py
 | 
			
		||||
msgid "The option configures the footer links on the flow executor pages."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										12182
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										12182
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -12,7 +12,8 @@
 | 
			
		||||
        "@floating-ui/dom": "^1.6.11",
 | 
			
		||||
        "@formatjs/intl-listformat": "^7.5.7",
 | 
			
		||||
        "@fortawesome/fontawesome-free": "^6.6.0",
 | 
			
		||||
        "@goauthentik/api": "^2025.2.4-1745325566",
 | 
			
		||||
        "@goauthentik/api": "^2025.2.4-1745519715",
 | 
			
		||||
        "@lit-labs/ssr": "3.2.2",
 | 
			
		||||
        "@lit/context": "^1.1.2",
 | 
			
		||||
        "@lit/localize": "^0.12.2",
 | 
			
		||||
        "@lit/reactive-element": "^2.0.4",
 | 
			
		||||
@ -53,7 +54,6 @@
 | 
			
		||||
        "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",
 | 
			
		||||
 | 
			
		||||
@ -4,17 +4,13 @@ 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";
 | 
			
		||||
@ -25,32 +21,25 @@ import "@goauthentik/elements/router/RouterOutlet";
 | 
			
		||||
import "@goauthentik/elements/sidebar/Sidebar";
 | 
			
		||||
import "@goauthentik/elements/sidebar/SidebarItem";
 | 
			
		||||
 | 
			
		||||
import { CSSResult, TemplateResult, css, html, nothing } from "lit";
 | 
			
		||||
import { CSSResult, TemplateResult, css, html } from "lit";
 | 
			
		||||
import { customElement, property, query, state } from "lit/decorators.js";
 | 
			
		||||
import { classMap } from "lit/directives/class-map.js";
 | 
			
		||||
 | 
			
		||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
 | 
			
		||||
import PFDrawer from "@patternfly/patternfly/components/Drawer/drawer.css";
 | 
			
		||||
import 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 { LicenseSummaryStatusEnum, SessionUser, UiThemeEnum } from "@goauthentik/api";
 | 
			
		||||
import { SessionUser, UiThemeEnum } from "@goauthentik/api";
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
    AdminSidebarEnterpriseEntries,
 | 
			
		||||
    AdminSidebarEntries,
 | 
			
		||||
    renderSidebarItems,
 | 
			
		||||
} from "./AdminSidebar.js";
 | 
			
		||||
import "./AdminSidebar";
 | 
			
		||||
 | 
			
		||||
if (process.env.NODE_ENV === "development") {
 | 
			
		||||
    await import("@goauthentik/esbuild-plugin-live-reload/client");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@customElement("ak-interface-admin")
 | 
			
		||||
export class AdminInterface extends WithLicenseSummary(AuthenticatedInterface) {
 | 
			
		||||
    //#region Properties
 | 
			
		||||
 | 
			
		||||
export class AdminInterface extends AuthenticatedInterface {
 | 
			
		||||
    @property({ type: Boolean })
 | 
			
		||||
    notificationDrawerOpen = getURLParam("notificationDrawerOpen", false);
 | 
			
		||||
 | 
			
		||||
@ -65,29 +54,12 @@ export class AdminInterface extends WithLicenseSummary(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,
 | 
			
		||||
@ -95,30 +67,23 @@ export class AdminInterface extends WithLicenseSummary(AuthenticatedInterface) {
 | 
			
		||||
                    z-index: auto !important;
 | 
			
		||||
                    background-color: transparent;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                .display-none {
 | 
			
		||||
                    display: none;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                .pf-c-page {
 | 
			
		||||
                    background-color: var(--pf-c-page--BackgroundColor) !important;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                :host([theme="dark"]) {
 | 
			
		||||
                /* Global page background colour */
 | 
			
		||||
                    .pf-c-page {
 | 
			
		||||
                :host([theme="dark"]) .pf-c-page {
 | 
			
		||||
                    --pf-c-page--BackgroundColor: var(--ak-dark-background);
 | 
			
		||||
                }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                ak-page-navbar {
 | 
			
		||||
                ak-enterprise-status,
 | 
			
		||||
                ak-version-banner {
 | 
			
		||||
                    grid-area: header;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                .ak-sidebar {
 | 
			
		||||
                ak-admin-sidebar {
 | 
			
		||||
                    grid-area: nav;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                .pf-c-drawer__panel {
 | 
			
		||||
                    z-index: var(--pf-global--ZIndex--xl);
 | 
			
		||||
                }
 | 
			
		||||
@ -126,23 +91,10 @@ export class AdminInterface extends WithLicenseSummary(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({
 | 
			
		||||
@ -156,14 +108,6 @@ export class AdminInterface extends WithLicenseSummary(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> {
 | 
			
		||||
@ -174,7 +118,6 @@ export class AdminInterface extends WithLicenseSummary(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/");
 | 
			
		||||
        }
 | 
			
		||||
@ -182,14 +125,10 @@ export class AdminInterface extends WithLicenseSummary(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,
 | 
			
		||||
@ -197,18 +136,11 @@ export class AdminInterface extends WithLicenseSummary(AuthenticatedInterface) {
 | 
			
		||||
 | 
			
		||||
        return html` <ak-locale-context>
 | 
			
		||||
            <div class="pf-c-page">
 | 
			
		||||
                <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>
 | 
			
		||||
 | 
			
		||||
                <ak-version-banner></ak-version-banner>
 | 
			
		||||
                <ak-admin-sidebar
 | 
			
		||||
                    class="pf-c-page__sidebar ${classMap(sidebarClasses)}"
 | 
			
		||||
                ></ak-admin-sidebar>
 | 
			
		||||
                <div class="pf-c-page__drawer">
 | 
			
		||||
                    <div class="pf-c-drawer ${classMap(drawerClasses)}">
 | 
			
		||||
                        <div class="pf-c-drawer__main">
 | 
			
		||||
 | 
			
		||||
@ -1,77 +1,132 @@
 | 
			
		||||
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 { repeat } from "lit/directives/repeat.js";
 | 
			
		||||
import { customElement, property, state } from "lit/decorators.js";
 | 
			
		||||
import { map } from "lit/directives/map.js";
 | 
			
		||||
 | 
			
		||||
// 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 = [
 | 
			
		||||
import { UiThemeEnum } from "@goauthentik/api";
 | 
			
		||||
import type { SessionUser, UserSelf } from "@goauthentik/api";
 | 
			
		||||
 | 
			
		||||
@customElement("ak-admin-sidebar")
 | 
			
		||||
export class AkAdminSidebar extends WithCapabilitiesConfig(WithVersion(AKElement)) {
 | 
			
		||||
    @property({ type: Boolean, reflect: true })
 | 
			
		||||
    open = true;
 | 
			
		||||
 | 
			
		||||
    @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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 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[],
 | 
			
		||||
];
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Recursively renders a sidebar entry.
 | 
			
		||||
 */
 | 
			
		||||
export function renderSidebarItem([
 | 
			
		||||
    path,
 | 
			
		||||
    label,
 | 
			
		||||
    attributes,
 | 
			
		||||
    children,
 | 
			
		||||
]: SidebarEntry): TemplateResult {
 | 
			
		||||
    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}
 | 
			
		||||
        ${children ? renderSidebarItems(children) : nothing}
 | 
			
		||||
    </ak-sidebar-item>`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 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[] = [
 | 
			
		||||
        // 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")]]
 | 
			
		||||
    ],
 | 
			
		||||
                ["/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")]]
 | 
			
		||||
    ],
 | 
			
		||||
                ["/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")]]
 | 
			
		||||
    ],
 | 
			
		||||
                ["/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")]]
 | 
			
		||||
    ],
 | 
			
		||||
                ["/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")]]
 | 
			
		||||
    ],
 | 
			
		||||
                ["/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})$`]],
 | 
			
		||||
@ -79,19 +134,53 @@ export const AdminSidebarEntries: readonly SidebarEntry[] = [
 | 
			
		||||
                ["/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")]]
 | 
			
		||||
    ],
 | 
			
		||||
                ["/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")]]
 | 
			
		||||
    ],
 | 
			
		||||
];
 | 
			
		||||
                ["/admin/settings", msg("Settings")]]],
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
// prettier-ignore
 | 
			
		||||
export const AdminSidebarEnterpriseEntries: readonly SidebarEntry[] = [
 | 
			
		||||
    [null, msg("Enterprise"), null, [
 | 
			
		||||
        ["/enterprise/licenses", msg("Licenses"), null]
 | 
			
		||||
    ],
 | 
			
		||||
]]
 | 
			
		||||
        // 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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
declare global {
 | 
			
		||||
    interface HTMLElementTagNameMap {
 | 
			
		||||
        "ak-admin-sidebar": AkAdminSidebar;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -94,13 +94,10 @@ export class AdminOverviewPage extends AdminOverviewBase {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render(): TemplateResult {
 | 
			
		||||
        const username = this.user?.user.name || this.user?.user.username;
 | 
			
		||||
        const name = this.user?.user.name ?? this.user?.user.username;
 | 
			
		||||
 | 
			
		||||
        return html` <ak-page-header
 | 
			
		||||
                header=${msg(str`Welcome, ${username || ""}.`)}
 | 
			
		||||
                description=${msg("General system status")}
 | 
			
		||||
                ?hasIcon=${false}
 | 
			
		||||
            >
 | 
			
		||||
        return html`<ak-page-header description=${msg("General system status")} ?hasIcon=${false}>
 | 
			
		||||
                <span slot="header"> ${msg(str`Welcome, ${name || ""}.`)} </span>
 | 
			
		||||
            </ak-page-header>
 | 
			
		||||
            <section class="pf-c-page__main-section">
 | 
			
		||||
                <div class="pf-l-grid pf-m-gutter">
 | 
			
		||||
 | 
			
		||||
@ -83,10 +83,13 @@ 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="${msg("System settings")}"> </ak-page-header>
 | 
			
		||||
            <ak-page-header icon="fa fa-cog" header="" description="">
 | 
			
		||||
                <span slot="header"> ${msg("System settings")} </span>
 | 
			
		||||
            </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">
 | 
			
		||||
 | 
			
		||||
@ -1,110 +1,26 @@
 | 
			
		||||
import type { Config as DOMPurifyConfig } from "dompurify";
 | 
			
		||||
import DOMPurify from "dompurify";
 | 
			
		||||
import { trustedTypes } from "trusted-types";
 | 
			
		||||
 | 
			
		||||
import { render } from "lit";
 | 
			
		||||
import { render } from "@lit-labs/ssr";
 | 
			
		||||
import { collectResult } from "@lit-labs/ssr/lib/render-result.js";
 | 
			
		||||
import { TemplateResult, html } 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;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 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);
 | 
			
		||||
 | 
			
		||||
    const result = container.innerHTML;
 | 
			
		||||
 | 
			
		||||
    return result;
 | 
			
		||||
export async function renderStatic(input: TemplateResult): Promise<string> {
 | 
			
		||||
    return await collectResult(render(input));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function purify(input: TemplateResult): TemplateResult {
 | 
			
		||||
    return html`${until(
 | 
			
		||||
        (async () => {
 | 
			
		||||
            const rendered = await renderStatic(input);
 | 
			
		||||
            const purified = DOMPurify.sanitize(rendered);
 | 
			
		||||
            return html`${unsafeHTML(purified)}`;
 | 
			
		||||
        })(),
 | 
			
		||||
    )}`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -17,13 +17,6 @@
 | 
			
		||||
 | 
			
		||||
    /* 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) {
 | 
			
		||||
 | 
			
		||||
@ -1,220 +0,0 @@
 | 
			
		||||
/**
 | 
			
		||||
 * @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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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.
 | 
			
		||||
 */
 | 
			
		||||
export function appendStyleSheet(
 | 
			
		||||
    insertions: CSSStyleSheet | Iterable<CSSStyleSheet>,
 | 
			
		||||
    ...styleParents: StyleSheetParent[]
 | 
			
		||||
): void {
 | 
			
		||||
    insertions = Array.isArray(insertions) ? insertions : [insertions];
 | 
			
		||||
 | 
			
		||||
    for (const nextStyleSheet of insertions) {
 | 
			
		||||
        for (const styleParent of styleParents) {
 | 
			
		||||
            if (styleParent.adoptedStyleSheets.includes(nextStyleSheet)) return;
 | 
			
		||||
 | 
			
		||||
            styleParent.adoptedStyleSheets = [...styleParent.adoptedStyleSheets, nextStyleSheet];
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Remove a stylesheet from the given roots, matching by referential equality.
 | 
			
		||||
 */
 | 
			
		||||
export function removeStyleSheet(
 | 
			
		||||
    currentStyleSheet: CSSStyleSheet,
 | 
			
		||||
    ...styleParents: StyleSheetParent[]
 | 
			
		||||
): void {
 | 
			
		||||
    for (const styleParent of styleParents) {
 | 
			
		||||
        const nextAdoptedStyleSheets = styleParent.adoptedStyleSheets.filter(
 | 
			
		||||
            (styleSheet) => styleSheet !== currentStyleSheet,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        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,
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
@ -1,200 +0,0 @@
 | 
			
		||||
/**
 | 
			
		||||
 * @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
 | 
			
		||||
@ -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-xl">
 | 
			
		||||
        return html`<div class="pf-c-page__header-tools-item pf-m-hidden pf-m-visible-on-lg">
 | 
			
		||||
            <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-xl">
 | 
			
		||||
        return html`<div class="pf-c-page__header-tools-item pf-m-hidden pf-m-visible-on-lg">
 | 
			
		||||
            <button
 | 
			
		||||
                class="pf-c-button pf-m-plain"
 | 
			
		||||
                type="button"
 | 
			
		||||
@ -156,7 +156,9 @@ 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();
 | 
			
		||||
@ -173,14 +175,6 @@ 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)
 | 
			
		||||
@ -212,13 +206,17 @@ export class NavigationButtons extends AKElement {
 | 
			
		||||
            </div>
 | 
			
		||||
            ${this.renderImpersonation()}
 | 
			
		||||
            ${this.userDisplayName != ""
 | 
			
		||||
                ? 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">
 | 
			
		||||
                ? 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">
 | 
			
		||||
                          ${this.userDisplayName}
 | 
			
		||||
                      </div>
 | 
			
		||||
                  </div>`
 | 
			
		||||
                : nothing}
 | 
			
		||||
            ${this.renderAvatar()}
 | 
			
		||||
            <img
 | 
			
		||||
                class="pf-c-avatar"
 | 
			
		||||
                src=${ifDefined(this.me?.user.avatar)}
 | 
			
		||||
                alt="${msg("Avatar image")}"
 | 
			
		||||
            />
 | 
			
		||||
        </div>`;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,125 +1,165 @@
 | 
			
		||||
import { EVENT_THEME_CHANGE } from "@goauthentik/common/constants";
 | 
			
		||||
import { globalAK } from "@goauthentik/common/global";
 | 
			
		||||
import {
 | 
			
		||||
    StyleSheetInit,
 | 
			
		||||
    StyleSheetParent,
 | 
			
		||||
    appendStyleSheet,
 | 
			
		||||
    createStyleSheetUnsafe,
 | 
			
		||||
    removeStyleSheet,
 | 
			
		||||
    resolveStyleSheetParent,
 | 
			
		||||
} from "@goauthentik/common/stylesheets";
 | 
			
		||||
import { ResolvedUITheme, createUIThemeEffect, resolveUITheme } from "@goauthentik/common/theme";
 | 
			
		||||
import { type ThemedElement } from "@goauthentik/common/theme";
 | 
			
		||||
import { UIConfig } from "@goauthentik/common/ui/config";
 | 
			
		||||
import { adaptCSS } from "@goauthentik/common/utils";
 | 
			
		||||
import { ensureCSSStyleSheet } from "@goauthentik/elements/utils/ensureCSSStyleSheet";
 | 
			
		||||
 | 
			
		||||
import { localized } from "@lit/localize";
 | 
			
		||||
import { CSSResultGroup, CSSResultOrNative, LitElement } from "lit";
 | 
			
		||||
import { property } from "lit/decorators.js";
 | 
			
		||||
import { LitElement, ReactiveElement } from "lit";
 | 
			
		||||
 | 
			
		||||
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 { CurrentBrand, UiThemeEnum } from "@goauthentik/api";
 | 
			
		||||
import { Config, CurrentBrand, UiThemeEnum } from "@goauthentik/api";
 | 
			
		||||
 | 
			
		||||
// Re-export the theme helpers
 | 
			
		||||
export { rootInterface } from "@goauthentik/common/theme";
 | 
			
		||||
type AkInterface = HTMLElement & {
 | 
			
		||||
    getTheme: () => Promise<UiThemeEnum>;
 | 
			
		||||
    brand?: CurrentBrand;
 | 
			
		||||
    uiConfig?: UIConfig;
 | 
			
		||||
    config?: Config;
 | 
			
		||||
    get activeTheme(): UiThemeEnum | undefined;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export interface AKElementInit {
 | 
			
		||||
    brand?: Partial<CurrentBrand>;
 | 
			
		||||
    styleParents?: StyleSheetParent[];
 | 
			
		||||
}
 | 
			
		||||
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);
 | 
			
		||||
 | 
			
		||||
@localized()
 | 
			
		||||
export class AKElement extends LitElement implements ThemedElement {
 | 
			
		||||
    /**
 | 
			
		||||
     * 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;
 | 
			
		||||
export class AKElement extends LitElement {
 | 
			
		||||
    _mediaMatcher?: MediaQueryList;
 | 
			
		||||
    _mediaMatcherHandler?: (ev?: MediaQueryListEvent) => void;
 | 
			
		||||
    _activeTheme?: UiThemeEnum;
 | 
			
		||||
 | 
			
		||||
    protected static readonly DarkColorSchemeStyleSheet = createStyleSheetUnsafe(ThemeDark);
 | 
			
		||||
 | 
			
		||||
    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);
 | 
			
		||||
    get activeTheme(): UiThemeEnum | undefined {
 | 
			
		||||
        return this._activeTheme;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    constructor(init?: AKElementInit) {
 | 
			
		||||
    constructor() {
 | 
			
		||||
        super();
 | 
			
		||||
 | 
			
		||||
        const config = globalAK();
 | 
			
		||||
        const { brand = config.brand, styleParents = [] } = init || {};
 | 
			
		||||
 | 
			
		||||
        this.activeTheme = resolveUITheme(brand?.uiTheme);
 | 
			
		||||
        this.#styleParents = styleParents;
 | 
			
		||||
 | 
			
		||||
        this.#customCSSStyleSheet = brand?.brandingCustomCss
 | 
			
		||||
            ? createStyleSheetUnsafe(brand.brandingCustomCss)
 | 
			
		||||
            : null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #styleParents: StyleSheetParent[] = [];
 | 
			
		||||
    #customCSSStyleSheet: CSSStyleSheet | null;
 | 
			
		||||
 | 
			
		||||
    #themeAbortController: AbortController | null = null;
 | 
			
		||||
 | 
			
		||||
    public disconnectedCallback(): void {
 | 
			
		||||
        super.disconnectedCallback();
 | 
			
		||||
        this.#themeAbortController?.abort();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected createRenderRoot(): HTMLElement | DocumentFragment {
 | 
			
		||||
        const renderRoot = super.createRenderRoot();
 | 
			
		||||
 | 
			
		||||
        const styleRoot = resolveStyleSheetParent(renderRoot);
 | 
			
		||||
        const styleParents = Array.from(
 | 
			
		||||
            new Set<StyleSheetParent>([styleRoot, ...this.#styleParents]),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        if (this.#customCSSStyleSheet) {
 | 
			
		||||
            console.debug(`authentik/element[${this.tagName.toLowerCase()}]: Adding custom CSS`);
 | 
			
		||||
 | 
			
		||||
            styleRoot.adoptedStyleSheets = [
 | 
			
		||||
    setInitialStyles(root: DocumentOrShadowRoot) {
 | 
			
		||||
        const styleRoot: DocumentOrShadowRoot = (
 | 
			
		||||
            "ShadyDOM" in window ? document : root
 | 
			
		||||
        ) as DocumentOrShadowRoot;
 | 
			
		||||
        styleRoot.adoptedStyleSheets = adaptCSS([
 | 
			
		||||
            ...styleRoot.adoptedStyleSheets,
 | 
			
		||||
                this.#customCSSStyleSheet,
 | 
			
		||||
            ensureCSSStyleSheet(AKGlobal),
 | 
			
		||||
            ensureCSSStyleSheet(OneDark),
 | 
			
		||||
        ]);
 | 
			
		||||
        this._initTheme(styleRoot);
 | 
			
		||||
        this._initCustomCSS(styleRoot);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected createRenderRoot() {
 | 
			
		||||
        this.fixElementStyles();
 | 
			
		||||
        const root = super.createRenderRoot();
 | 
			
		||||
        this.setInitialStyles(root as unknown as DocumentOrShadowRoot);
 | 
			
		||||
        return root;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getTheme(): Promise<UiThemeEnum> {
 | 
			
		||||
        return rootInterface()?.getTheme() || UiThemeEnum.Automatic;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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;
 | 
			
		||||
        }
 | 
			
		||||
        const sheet = await new CSSStyleSheet().replace(brand.brandingCustomCss);
 | 
			
		||||
        root.adoptedStyleSheets = [...root.adoptedStyleSheets, sheet];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _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),
 | 
			
		||||
                ];
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        this.#themeAbortController = new AbortController();
 | 
			
		||||
 | 
			
		||||
        createUIThemeEffect(
 | 
			
		||||
            (currentUITheme) => {
 | 
			
		||||
                if (currentUITheme === UiThemeEnum.Dark) {
 | 
			
		||||
                    appendStyleSheet(AKElement.DarkColorSchemeStyleSheet, ...styleParents);
 | 
			
		||||
                } else {
 | 
			
		||||
                    removeStyleSheet(AKElement.DarkColorSchemeStyleSheet, ...styleParents);
 | 
			
		||||
                }
 | 
			
		||||
                this.activeTheme = currentUITheme;
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                signal: this.#themeAbortController.signal,
 | 
			
		||||
            },
 | 
			
		||||
            if (oldStylesheet) {
 | 
			
		||||
                root.adoptedStyleSheets = root.adoptedStyleSheets.filter(
 | 
			
		||||
                    (v) => v !== oldStylesheet,
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
        return renderRoot;
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        this._activeTheme = theme;
 | 
			
		||||
        this.requestUpdate();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,5 @@
 | 
			
		||||
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";
 | 
			
		||||
 | 
			
		||||
@ -10,12 +9,14 @@ 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<ThemedElement>;
 | 
			
		||||
    host!: ReactiveElementHost<AkInterface>;
 | 
			
		||||
 | 
			
		||||
    context!: ContextProvider<{ __context__: CurrentBrand | undefined }>;
 | 
			
		||||
 | 
			
		||||
    constructor(host: ReactiveElementHost<ThemedElement>) {
 | 
			
		||||
    constructor(host: ReactiveElementHost<AkInterface>) {
 | 
			
		||||
        this.host = host;
 | 
			
		||||
        this.context = new ContextProvider(this.host, {
 | 
			
		||||
            context: authentikBrandContext,
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,6 @@
 | 
			
		||||
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";
 | 
			
		||||
 | 
			
		||||
@ -11,12 +10,14 @@ 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<ThemedElement>;
 | 
			
		||||
    host!: ReactiveElementHost<AkInterface>;
 | 
			
		||||
 | 
			
		||||
    context!: ContextProvider<{ __context__: Config | undefined }>;
 | 
			
		||||
 | 
			
		||||
    constructor(host: ReactiveElementHost<ThemedElement>) {
 | 
			
		||||
    constructor(host: ReactiveElementHost<AkInterface>) {
 | 
			
		||||
        this.host = host;
 | 
			
		||||
        this.context = new ContextProvider(this.host, {
 | 
			
		||||
            context: authentikConfigContext,
 | 
			
		||||
 | 
			
		||||
@ -1,85 +1,107 @@
 | 
			
		||||
import {
 | 
			
		||||
    appendStyleSheet,
 | 
			
		||||
    createStyleSheetUnsafe,
 | 
			
		||||
    resolveStyleSheetParent,
 | 
			
		||||
} from "@goauthentik/common/stylesheets";
 | 
			
		||||
import { ThemedElement } from "@goauthentik/common/theme";
 | 
			
		||||
import { UIConfig } from "@goauthentik/common/ui/config";
 | 
			
		||||
import { AKElement, AKElementInit } from "@goauthentik/elements/Base";
 | 
			
		||||
import { UIConfig, uiConfig } from "@goauthentik/common/ui/config";
 | 
			
		||||
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 abstract class Interface extends AKElement implements ThemedElement {
 | 
			
		||||
    protected static readonly PFBaseStyleSheet = createStyleSheetUnsafe(PFBase);
 | 
			
		||||
export class Interface extends AKElement implements AkInterface {
 | 
			
		||||
    [brandContext]!: BrandContextController;
 | 
			
		||||
 | 
			
		||||
    [brandContext]: BrandContextController;
 | 
			
		||||
    [configContext]!: ConfigContextController;
 | 
			
		||||
 | 
			
		||||
    [configContext]: ConfigContextController;
 | 
			
		||||
 | 
			
		||||
    [modalController]: ModalOrchestrationController;
 | 
			
		||||
    [modalController]!: ModalOrchestrationController;
 | 
			
		||||
 | 
			
		||||
    @state()
 | 
			
		||||
    public config?: Config;
 | 
			
		||||
    uiConfig?: UIConfig;
 | 
			
		||||
 | 
			
		||||
    @state()
 | 
			
		||||
    public brand?: CurrentBrand;
 | 
			
		||||
    config?: Config;
 | 
			
		||||
 | 
			
		||||
    constructor({ styleParents = [], ...init }: AKElementInit = {}) {
 | 
			
		||||
        const styleParent = resolveStyleSheetParent(document);
 | 
			
		||||
    @state()
 | 
			
		||||
    brand?: CurrentBrand;
 | 
			
		||||
 | 
			
		||||
        super({
 | 
			
		||||
            ...init,
 | 
			
		||||
            styleParents: [styleParent, ...styleParents],
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        this.dataset.akInterfaceRoot = this.tagName.toLowerCase();
 | 
			
		||||
 | 
			
		||||
        appendStyleSheet(Interface.PFBaseStyleSheet, styleParent);
 | 
			
		||||
    constructor() {
 | 
			
		||||
        super();
 | 
			
		||||
        document.adoptedStyleSheets = [...document.adoptedStyleSheets, ensureCSSStyleSheet(PFBase)];
 | 
			
		||||
        this._initContexts();
 | 
			
		||||
        this.dataset.akInterfaceRoot = "true";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _initContexts() {
 | 
			
		||||
        this[brandContext] = 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 interface AkAuthenticatedInterface extends ThemedElement {
 | 
			
		||||
export type AkAuthenticatedInterface = AkInterface & {
 | 
			
		||||
    licenseSummary?: LicenseSummary;
 | 
			
		||||
    version?: Version;
 | 
			
		||||
}
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const enterpriseContext = Symbol("enterpriseContext");
 | 
			
		||||
 | 
			
		||||
export class AuthenticatedInterface extends Interface implements AkAuthenticatedInterface {
 | 
			
		||||
export class AuthenticatedInterface extends Interface {
 | 
			
		||||
    [enterpriseContext]!: EnterpriseContextController;
 | 
			
		||||
    [versionContext]!: VersionContextController;
 | 
			
		||||
 | 
			
		||||
    @state()
 | 
			
		||||
    public uiConfig?: UIConfig;
 | 
			
		||||
    licenseSummary?: LicenseSummary;
 | 
			
		||||
 | 
			
		||||
    @state()
 | 
			
		||||
    public licenseSummary?: LicenseSummary;
 | 
			
		||||
    version?: Version;
 | 
			
		||||
 | 
			
		||||
    @state()
 | 
			
		||||
    public version?: Version;
 | 
			
		||||
 | 
			
		||||
    constructor(init?: AKElementInit) {
 | 
			
		||||
        super(init);
 | 
			
		||||
    constructor() {
 | 
			
		||||
        super();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _initContexts(): void {
 | 
			
		||||
        super._initContexts();
 | 
			
		||||
        this[enterpriseContext] = new EnterpriseContextController(this);
 | 
			
		||||
        this[versionContext] = new VersionContextController(this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -5,23 +5,20 @@ import {
 | 
			
		||||
} from "@goauthentik/common/constants";
 | 
			
		||||
import { globalAK } from "@goauthentik/common/global";
 | 
			
		||||
import { currentInterface } from "@goauthentik/common/sentry";
 | 
			
		||||
import { UIConfig, UserDisplay, getConfigForUser } from "@goauthentik/common/ui/config";
 | 
			
		||||
import { UIConfig, UserDisplay, uiConfig } 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 { DefaultBrand } from "@goauthentik/elements/sidebar/SidebarBrand";
 | 
			
		||||
import { themeImage } from "@goauthentik/elements/utils/images";
 | 
			
		||||
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
 | 
			
		||||
 | 
			
		||||
import { msg } from "@lit/localize";
 | 
			
		||||
import { CSSResult, LitElement, TemplateResult, css, html, nothing } from "lit";
 | 
			
		||||
import { CSSResult, 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";
 | 
			
		||||
@ -29,52 +26,34 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
 | 
			
		||||
 | 
			
		||||
import { SessionUser } from "@goauthentik/api";
 | 
			
		||||
 | 
			
		||||
//#region Page Navbar
 | 
			
		||||
 | 
			
		||||
export interface PageNavbarDetails {
 | 
			
		||||
    header?: string;
 | 
			
		||||
    description?: string;
 | 
			
		||||
@customElement("ak-page-header")
 | 
			
		||||
export class PageHeader extends WithBrandConfig(AKElement) {
 | 
			
		||||
    @property()
 | 
			
		||||
    icon?: string;
 | 
			
		||||
    iconImage?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 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
 | 
			
		||||
    @property({ type: Boolean })
 | 
			
		||||
    iconImage = false;
 | 
			
		||||
 | 
			
		||||
    private static elementRef: AKPageNavbar | null = null;
 | 
			
		||||
    @property()
 | 
			
		||||
    header = "";
 | 
			
		||||
 | 
			
		||||
    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;
 | 
			
		||||
        }
 | 
			
		||||
    @property()
 | 
			
		||||
    description?: string;
 | 
			
		||||
 | 
			
		||||
        const { header, description, icon, iconImage } = detail;
 | 
			
		||||
    @property({ type: Boolean })
 | 
			
		||||
    hasIcon = true;
 | 
			
		||||
 | 
			
		||||
        elementRef.header = header;
 | 
			
		||||
        elementRef.description = description;
 | 
			
		||||
        elementRef.icon = icon;
 | 
			
		||||
        elementRef.iconImage = iconImage || false;
 | 
			
		||||
        elementRef.hasIcon = !!icon;
 | 
			
		||||
    };
 | 
			
		||||
    @state()
 | 
			
		||||
    me?: SessionUser;
 | 
			
		||||
 | 
			
		||||
    @state()
 | 
			
		||||
    uiConfig!: UIConfig;
 | 
			
		||||
 | 
			
		||||
    static get styles(): CSSResult[] {
 | 
			
		||||
        return [
 | 
			
		||||
            PFBase,
 | 
			
		||||
            PFButton,
 | 
			
		||||
            PFPage,
 | 
			
		||||
            PFDrawer,
 | 
			
		||||
 | 
			
		||||
            PFNotificationBadge,
 | 
			
		||||
            PFContent,
 | 
			
		||||
            PFAvatar,
 | 
			
		||||
@ -84,313 +63,127 @@ export class AKPageNavbar extends WithBrandConfig(AKElement) implements PageNavb
 | 
			
		||||
                    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([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 {
 | 
			
		||||
                .bar {
 | 
			
		||||
                    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: 6rem;
 | 
			
		||||
 | 
			
		||||
                    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 (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%;
 | 
			
		||||
                    min-height: 114px;
 | 
			
		||||
                    max-height: 114px;
 | 
			
		||||
                    background-color: var(--pf-c-page--BackgroundColor);
 | 
			
		||||
                }
 | 
			
		||||
                .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;
 | 
			
		||||
                        padding-block-end: var(--pf-global--spacer--md);
 | 
			
		||||
 | 
			
		||||
                        @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);
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                .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);
 | 
			
		||||
 | 
			
		||||
                .pf-c-page__main-section {
 | 
			
		||||
                    flex-grow: 1;
 | 
			
		||||
                    flex-shrink: 1;
 | 
			
		||||
                    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: 1.5rem;
 | 
			
		||||
                    font-size: 24px;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                .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;
 | 
			
		||||
                }
 | 
			
		||||
            `,
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    //#endregion
 | 
			
		||||
    constructor() {
 | 
			
		||||
        super();
 | 
			
		||||
        window.addEventListener(EVENT_WS_MESSAGE, () => {
 | 
			
		||||
            this.firstUpdated();
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    //#region Properties
 | 
			
		||||
    async firstUpdated() {
 | 
			
		||||
        this.me = await me();
 | 
			
		||||
        this.uiConfig = await uiConfig();
 | 
			
		||||
        this.uiConfig.navbar.userDisplay = UserDisplay.none;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @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) {
 | 
			
		||||
    setTitle(header?: string) {
 | 
			
		||||
        const currentIf = currentInterface();
 | 
			
		||||
        let title = this.brand?.brandingTitle || TITLE_DEFAULT;
 | 
			
		||||
 | 
			
		||||
        if (currentIf === "admin") {
 | 
			
		||||
            title = `${msg("Admin")} - ${title}`;
 | 
			
		||||
        }
 | 
			
		||||
        // Prepend the header to the title
 | 
			
		||||
        if (header) {
 | 
			
		||||
        if (header !== undefined && header !== "") {
 | 
			
		||||
            title = `${header} - ${title}`;
 | 
			
		||||
        }
 | 
			
		||||
        document.title = title;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #toggleSidebar() {
 | 
			
		||||
        this.open = !this.open;
 | 
			
		||||
    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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    renderIcon() {
 | 
			
		||||
        if (this.icon) {
 | 
			
		||||
            if (this.iconImage && !this.icon.startsWith("fa://")) {
 | 
			
		||||
                return html`<img class="pf-icon" src="${this.icon}" alt="page icon" />`;
 | 
			
		||||
            }
 | 
			
		||||
            const icon = this.icon.replaceAll("fa://", "fa ");
 | 
			
		||||
            return html`<i class=${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,
 | 
			
		||||
                        }),
 | 
			
		||||
                    );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    //#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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    //#endregion
 | 
			
		||||
 | 
			
		||||
    //#region Render
 | 
			
		||||
 | 
			
		||||
    renderIcon() {
 | 
			
		||||
        if (this.icon) {
 | 
			
		||||
            if (this.iconImage && !this.icon.startsWith("fa://")) {
 | 
			
		||||
                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="accent-icon ${icon}"></i>`;
 | 
			
		||||
        }
 | 
			
		||||
        return nothing;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render(): TemplateResult {
 | 
			
		||||
        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">
 | 
			
		||||
            <section class="pf-c-page__main-section pf-m-light">
 | 
			
		||||
                <div class="pf-c-content">
 | 
			
		||||
                    <h1>
 | 
			
		||||
                        ${this.hasIcon
 | 
			
		||||
                            ? html`<slot name="icon">${this.renderIcon()}</slot>`
 | 
			
		||||
                            ? html`<slot name="icon">${this.renderIcon()}</slot> `
 | 
			
		||||
                            : nothing}
 | 
			
		||||
                        ${this.header}
 | 
			
		||||
                        <slot name="header">${this.header}</slot>
 | 
			
		||||
                    </h1>
 | 
			
		||||
                    ${this.description ? html`<p>${this.description}</p>` : html``}
 | 
			
		||||
                </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">
 | 
			
		||||
                <div class="pf-c-page__header-tools-group">
 | 
			
		||||
                        <ak-nav-buttons .uiConfig=${this.uiConfig} .me=${this.session}>
 | 
			
		||||
                    <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/"
 | 
			
		||||
@ -400,76 +193,13 @@ export class AKPageNavbar extends WithBrandConfig(AKElement) implements PageNavb
 | 
			
		||||
                        </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,
 | 
			
		||||
        });
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>`;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
//#endregion
 | 
			
		||||
 | 
			
		||||
declare global {
 | 
			
		||||
    interface HTMLElementTagNameMap {
 | 
			
		||||
        "ak-page-header": AKPageHeader;
 | 
			
		||||
        "ak-page-navbar": AKPageNavbar;
 | 
			
		||||
        "ak-page-header": PageHeader;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -22,7 +22,6 @@ 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,
 | 
			
		||||
@ -36,7 +35,10 @@ 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;
 | 
			
		||||
@ -68,6 +70,7 @@ 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>
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,17 @@
 | 
			
		||||
import { EVENT_SIDEBAR_TOGGLE } from "@goauthentik/common/constants";
 | 
			
		||||
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 } 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
 | 
			
		||||
@ -14,3 +28,79 @@ export const DefaultBrand: CurrentBrand = {
 | 
			
		||||
    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 {
 | 
			
		||||
        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>
 | 
			
		||||
                  `
 | 
			
		||||
                : html``}
 | 
			
		||||
            <a href="#/" class="pf-c-page__header-brand-link">
 | 
			
		||||
                <div class="pf-c-brand ak-brand">
 | 
			
		||||
                    <img
 | 
			
		||||
                        src=${themeImage(this.brand?.brandingLogo ?? DefaultBrand.brandingLogo)}
 | 
			
		||||
                        alt="${msg("authentik Logo")}"
 | 
			
		||||
                        loading="lazy"
 | 
			
		||||
                    />
 | 
			
		||||
                </div>
 | 
			
		||||
            </a>`;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
declare global {
 | 
			
		||||
    interface HTMLElementTagNameMap {
 | 
			
		||||
        "ak-sidebar-brand": SidebarBrand;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,20 +1,19 @@
 | 
			
		||||
import {
 | 
			
		||||
    appendStyleSheet,
 | 
			
		||||
    assertAdoptableStyleSheetParent,
 | 
			
		||||
} 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) => {
 | 
			
		||||
    assertAdoptableStyleSheetParent(document);
 | 
			
		||||
 | 
			
		||||
    appendStyleSheet([PFBase, AKGlobal], document);
 | 
			
		||||
    document.adoptedStyleSheets = [
 | 
			
		||||
        ...document.adoptedStyleSheets,
 | 
			
		||||
        ensureCSSStyleSheet(PFBase),
 | 
			
		||||
        ensureCSSStyleSheet(AKGlobal),
 | 
			
		||||
    ];
 | 
			
		||||
    return litRender(body, document.body);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -1,14 +1,9 @@
 | 
			
		||||
import { AKElement } from "@goauthentik/elements/Base";
 | 
			
		||||
 | 
			
		||||
import { type LitElement, type ReactiveControllerHost, type TemplateResult, nothing } from "lit";
 | 
			
		||||
import "lit";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 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 ReactiveElementHost<T = AKElement> = Partial<ReactiveControllerHost> & T;
 | 
			
		||||
 | 
			
		||||
export type AbstractLitElementConstructor = abstract new (...args: never[]) => LitElement;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										35
									
								
								web/src/elements/utils/ensureCSSStyleSheet.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								web/src/elements/utils/ensureCSSStyleSheet.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,35 @@
 | 
			
		||||
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;
 | 
			
		||||
@ -1,55 +0,0 @@
 | 
			
		||||
/**
 | 
			
		||||
 * @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,8 +1,13 @@
 | 
			
		||||
import { resolveUITheme } from "@goauthentik/common/theme";
 | 
			
		||||
import { rootInterface } from "@goauthentik/elements/Base";
 | 
			
		||||
import { QUERY_MEDIA_COLOR_LIGHT, rootInterface } from "@goauthentik/elements/Base";
 | 
			
		||||
 | 
			
		||||
import { UiThemeEnum } from "@goauthentik/api";
 | 
			
		||||
 | 
			
		||||
export function themeImage(rawPath: string) {
 | 
			
		||||
    const enabledTheme = rootInterface()?.activeTheme || resolveUITheme();
 | 
			
		||||
 | 
			
		||||
    let enabledTheme = rootInterface()?.activeTheme;
 | 
			
		||||
    if (!enabledTheme || enabledTheme === UiThemeEnum.Automatic) {
 | 
			
		||||
        enabledTheme = window.matchMedia(QUERY_MEDIA_COLOR_LIGHT).matches
 | 
			
		||||
            ? UiThemeEnum.Light
 | 
			
		||||
            : UiThemeEnum.Dark;
 | 
			
		||||
    }
 | 
			
		||||
    return rawPath.replaceAll("%(theme)s", enabledTheme);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -46,6 +46,7 @@ import {
 | 
			
		||||
    FlowsApi,
 | 
			
		||||
    ResponseError,
 | 
			
		||||
    ShellChallenge,
 | 
			
		||||
    UiThemeEnum,
 | 
			
		||||
} from "@goauthentik/api";
 | 
			
		||||
 | 
			
		||||
@customElement("ak-flow-executor")
 | 
			
		||||
@ -199,6 +200,10 @@ export class FlowExecutor extends Interface implements StageHost {
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getTheme(): Promise<UiThemeEnum> {
 | 
			
		||||
        return globalAK()?.brand.uiTheme || UiThemeEnum.Automatic;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async submit(
 | 
			
		||||
        payload?: FlowChallengeResponseRequest,
 | 
			
		||||
        options?: SubmitOptions,
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
import { BrandedHTMLPolicy, sanitizeHTML } from "@goauthentik/common/purify";
 | 
			
		||||
import { purify } from "@goauthentik/common/purify";
 | 
			
		||||
import { AKElement } from "@goauthentik/elements/Base.js";
 | 
			
		||||
 | 
			
		||||
import { msg } from "@lit/localize";
 | 
			
		||||
@ -21,6 +21,8 @@ const styles = css`
 | 
			
		||||
    }
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const poweredBy: FooterLink = { name: msg("Powered by authentik"), href: null };
 | 
			
		||||
 | 
			
		||||
@customElement("ak-brand-links")
 | 
			
		||||
export class BrandLinks extends AKElement {
 | 
			
		||||
    static get styles() {
 | 
			
		||||
@ -31,21 +33,13 @@ export class BrandLinks extends AKElement {
 | 
			
		||||
    links: FooterLink[] = [];
 | 
			
		||||
 | 
			
		||||
    render() {
 | 
			
		||||
        const links = [...(this.links ?? [])];
 | 
			
		||||
 | 
			
		||||
        const links = [...(this.links ?? []), poweredBy];
 | 
			
		||||
        return html` <ul class="pf-c-list pf-m-inline">
 | 
			
		||||
            ${map(links, (link) => {
 | 
			
		||||
                const children = sanitizeHTML(BrandedHTMLPolicy, link.name);
 | 
			
		||||
 | 
			
		||||
                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>
 | 
			
		||||
            ${map(links, (link) =>
 | 
			
		||||
                link.href
 | 
			
		||||
                    ? purify(html`<li><a href="${link.href}">${link.name}</a></li>`)
 | 
			
		||||
                    : html`<li><span>${link.name}</span></li>`,
 | 
			
		||||
            )}
 | 
			
		||||
        </ul>`;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,16 +1,15 @@
 | 
			
		||||
/// <reference types="@hcaptcha/types"/>
 | 
			
		||||
/// <reference types="turnstile-types"/>
 | 
			
		||||
import { renderStaticHTMLUnsafe } from "@goauthentik/common/purify";
 | 
			
		||||
///<reference types="@hcaptcha/types"/>
 | 
			
		||||
import { renderStatic } from "@goauthentik/common/purify";
 | 
			
		||||
import "@goauthentik/elements/EmptyState";
 | 
			
		||||
import { akEmptyState } from "@goauthentik/elements/EmptyState";
 | 
			
		||||
import { bound } from "@goauthentik/elements/decorators/bound";
 | 
			
		||||
import "@goauthentik/elements/forms/FormElement";
 | 
			
		||||
import { createIFrameHTMLWrapper } from "@goauthentik/elements/utils/iframe";
 | 
			
		||||
import { ListenerController } from "@goauthentik/elements/utils/listenerController.js";
 | 
			
		||||
import { randomId } from "@goauthentik/elements/utils/randomId";
 | 
			
		||||
import "@goauthentik/flow/FormStatic";
 | 
			
		||||
import { BaseStage } from "@goauthentik/flow/stages/base";
 | 
			
		||||
import { P, match } from "ts-pattern";
 | 
			
		||||
import type * as _ from "turnstile-types";
 | 
			
		||||
 | 
			
		||||
import { msg } from "@lit/localize";
 | 
			
		||||
import { CSSResult, PropertyValues, TemplateResult, css, html, nothing } from "lit";
 | 
			
		||||
@ -57,14 +56,18 @@ type CaptchaHandler = {
 | 
			
		||||
// 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
 | 
			
		||||
// rendering.
 | 
			
		||||
function iframeTemplate(children: TemplateResult, challengeURL: string): TemplateResult {
 | 
			
		||||
    return html` ${children}
 | 
			
		||||
 | 
			
		||||
const iframeTemplate = (captchaElement: TemplateResult, challengeUrl: string) =>
 | 
			
		||||
    html`<!doctype html>
 | 
			
		||||
        <head>
 | 
			
		||||
            <html>
 | 
			
		||||
                <body style="display:flex;flex-direction:row;justify-content:center;">
 | 
			
		||||
                    ${captchaElement}
 | 
			
		||||
                    <script>
 | 
			
		||||
                        new ResizeObserver((entries) => {
 | 
			
		||||
                            const height =
 | 
			
		||||
                                document.body.offsetHeight +
 | 
			
		||||
                                parseFloat(getComputedStyle(document.body).fontSize) * 2;
 | 
			
		||||
 | 
			
		||||
                            window.parent.postMessage({
 | 
			
		||||
                                message: "resize",
 | 
			
		||||
                                source: "goauthentik.io",
 | 
			
		||||
@ -73,20 +76,20 @@ function iframeTemplate(children: TemplateResult, challengeURL: string): Templat
 | 
			
		||||
                            });
 | 
			
		||||
                        }).observe(document.querySelector(".ak-captcha-container"));
 | 
			
		||||
                    </script>
 | 
			
		||||
 | 
			
		||||
        <script src=${challengeURL}></script>
 | 
			
		||||
 | 
			
		||||
                    <script src=${challengeUrl}></script>
 | 
			
		||||
                    <script>
 | 
			
		||||
                        function callback(token) {
 | 
			
		||||
                            window.parent.postMessage({
 | 
			
		||||
                                message: "captcha",
 | 
			
		||||
                                source: "goauthentik.io",
 | 
			
		||||
                                context: "flow-executor",
 | 
			
		||||
                    token,
 | 
			
		||||
                                token: token,
 | 
			
		||||
                            });
 | 
			
		||||
                        }
 | 
			
		||||
        </script>`;
 | 
			
		||||
}
 | 
			
		||||
                    </script>
 | 
			
		||||
                </body>
 | 
			
		||||
            </html>
 | 
			
		||||
        </head>`;
 | 
			
		||||
 | 
			
		||||
@customElement("ak-stage-captcha")
 | 
			
		||||
export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeResponseRequest> {
 | 
			
		||||
@ -302,25 +305,11 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async renderFrame(captchaElement: TemplateResult) {
 | 
			
		||||
        const { contentDocument } = this.captchaFrame || {};
 | 
			
		||||
 | 
			
		||||
        if (!contentDocument) {
 | 
			
		||||
            console.debug(
 | 
			
		||||
                "authentik/stages/captcha: unable to render captcha frame, no contentDocument",
 | 
			
		||||
        this.captchaFrame.contentWindow?.document.open();
 | 
			
		||||
        this.captchaFrame.contentWindow?.document.write(
 | 
			
		||||
            await renderStatic(iframeTemplate(captchaElement, this.challenge.jsUrl)),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        contentDocument.open();
 | 
			
		||||
 | 
			
		||||
        contentDocument.write(
 | 
			
		||||
            createIFrameHTMLWrapper(
 | 
			
		||||
                renderStaticHTMLUnsafe(iframeTemplate(captchaElement, this.challenge.jsUrl)),
 | 
			
		||||
            ),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        contentDocument.close();
 | 
			
		||||
        this.captchaFrame.contentWindow?.document.close();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    renderBody() {
 | 
			
		||||
 | 
			
		||||
@ -3,6 +3,7 @@ import "rapidoc";
 | 
			
		||||
 | 
			
		||||
import { CSRFHeaderName } from "@goauthentik/common/api/config";
 | 
			
		||||
import { EVENT_THEME_CHANGE } from "@goauthentik/common/constants";
 | 
			
		||||
import { globalAK } from "@goauthentik/common/global";
 | 
			
		||||
import { first, getCookie } from "@goauthentik/common/utils";
 | 
			
		||||
import { Interface } from "@goauthentik/elements/Interface";
 | 
			
		||||
import "@goauthentik/elements/ak-locale-context";
 | 
			
		||||
@ -61,6 +62,10 @@ export class APIBrowser extends Interface {
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getTheme(): Promise<UiThemeEnum> {
 | 
			
		||||
        return globalAK()?.brand.uiTheme || UiThemeEnum.Automatic;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render(): TemplateResult {
 | 
			
		||||
        return html`
 | 
			
		||||
            <ak-locale-context>
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
import { globalAK } from "@goauthentik/common/global";
 | 
			
		||||
import { Interface } from "@goauthentik/elements/Interface";
 | 
			
		||||
 | 
			
		||||
import { msg } from "@lit/localize";
 | 
			
		||||
@ -9,6 +10,8 @@ import PFPage from "@patternfly/patternfly/components/Page/page.css";
 | 
			
		||||
import PFSpinner from "@patternfly/patternfly/components/Spinner/spinner.css";
 | 
			
		||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
 | 
			
		||||
 | 
			
		||||
import { UiThemeEnum } from "@goauthentik/api";
 | 
			
		||||
 | 
			
		||||
@customElement("ak-loading")
 | 
			
		||||
export class Loading extends Interface {
 | 
			
		||||
    static get styles(): CSSResult[] {
 | 
			
		||||
@ -25,7 +28,7 @@ export class Loading extends Interface {
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    registerContexts(): void {
 | 
			
		||||
    _initContexts(): void {
 | 
			
		||||
        // Stub function to avoid making API requests for things we don't need. The `Interface` base class loads
 | 
			
		||||
        // a bunch of data that is used globally by various things, however this is an interface that is shown
 | 
			
		||||
        // very briefly and we don't need any of that data.
 | 
			
		||||
@ -35,6 +38,10 @@ export class Loading extends Interface {
 | 
			
		||||
        // Stub function to avoid fetching custom CSS.
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getTheme(): Promise<UiThemeEnum> {
 | 
			
		||||
        return globalAK()?.brand.uiTheme || UiThemeEnum.Automatic;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render(): TemplateResult {
 | 
			
		||||
        return html` <section
 | 
			
		||||
            class="ak-static-page pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl"
 | 
			
		||||
 | 
			
		||||
@ -1,9 +1,18 @@
 | 
			
		||||
import { FlowExecutor } from "@goauthentik/flow/FlowExecutor";
 | 
			
		||||
 | 
			
		||||
import { customElement } from "lit/decorators.js";
 | 
			
		||||
import { customElement, property } from "lit/decorators.js";
 | 
			
		||||
 | 
			
		||||
import { UiThemeEnum } from "@goauthentik/api";
 | 
			
		||||
 | 
			
		||||
@customElement("ak-storybook-interface-flow")
 | 
			
		||||
export class StoryFlowInterface extends FlowExecutor {}
 | 
			
		||||
export class StoryFlowInterface extends FlowExecutor {
 | 
			
		||||
    @property()
 | 
			
		||||
    storyTheme: UiThemeEnum = UiThemeEnum.Dark;
 | 
			
		||||
 | 
			
		||||
    async getTheme(): Promise<UiThemeEnum> {
 | 
			
		||||
        return this.storyTheme;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
declare global {
 | 
			
		||||
    interface HTMLElementTagNameMap {
 | 
			
		||||
 | 
			
		||||
@ -1,9 +1,18 @@
 | 
			
		||||
import { Interface } from "@goauthentik/elements/Interface";
 | 
			
		||||
 | 
			
		||||
import { customElement } from "lit/decorators.js";
 | 
			
		||||
import { customElement, property } from "lit/decorators.js";
 | 
			
		||||
 | 
			
		||||
import { UiThemeEnum } from "@goauthentik/api";
 | 
			
		||||
 | 
			
		||||
@customElement("ak-storybook-interface")
 | 
			
		||||
export class StoryInterface extends Interface {}
 | 
			
		||||
export class StoryInterface extends Interface {
 | 
			
		||||
    @property()
 | 
			
		||||
    storyTheme: UiThemeEnum = UiThemeEnum.Dark;
 | 
			
		||||
 | 
			
		||||
    async getTheme(): Promise<UiThemeEnum> {
 | 
			
		||||
        return this.storyTheme;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
declare global {
 | 
			
		||||
    interface HTMLElementTagNameMap {
 | 
			
		||||
 | 
			
		||||
@ -6,7 +6,7 @@ import {
 | 
			
		||||
} from "@goauthentik/common/constants";
 | 
			
		||||
import { globalAK } from "@goauthentik/common/global";
 | 
			
		||||
import { configureSentry } from "@goauthentik/common/sentry";
 | 
			
		||||
import { UIConfig, getConfigForUser } from "@goauthentik/common/ui/config";
 | 
			
		||||
import { UIConfig } from "@goauthentik/common/ui/config";
 | 
			
		||||
import { me } from "@goauthentik/common/users";
 | 
			
		||||
import { WebsocketClient } from "@goauthentik/common/ws";
 | 
			
		||||
import "@goauthentik/components/ak-nav-buttons";
 | 
			
		||||
@ -292,7 +292,6 @@ export class UserInterface extends AuthenticatedInterface {
 | 
			
		||||
 | 
			
		||||
    async connectedCallback() {
 | 
			
		||||
        super.connectedCallback();
 | 
			
		||||
 | 
			
		||||
        window.addEventListener(EVENT_NOTIFICATION_DRAWER_TOGGLE, this.toggleNotificationDrawer);
 | 
			
		||||
        window.addEventListener(EVENT_API_DRAWER_TOGGLE, this.toggleApiDrawer);
 | 
			
		||||
        window.addEventListener(EVENT_WS_MESSAGE, this.fetchConfigurationDetails);
 | 
			
		||||
@ -302,7 +301,6 @@ export class UserInterface extends AuthenticatedInterface {
 | 
			
		||||
        window.removeEventListener(EVENT_NOTIFICATION_DRAWER_TOGGLE, this.toggleNotificationDrawer);
 | 
			
		||||
        window.removeEventListener(EVENT_API_DRAWER_TOGGLE, this.toggleApiDrawer);
 | 
			
		||||
        window.removeEventListener(EVENT_WS_MESSAGE, this.fetchConfigurationDetails);
 | 
			
		||||
 | 
			
		||||
        super.disconnectedCallback();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -321,10 +319,8 @@ export class UserInterface extends AuthenticatedInterface {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fetchConfigurationDetails() {
 | 
			
		||||
        me().then((session: SessionUser) => {
 | 
			
		||||
            this.me = session;
 | 
			
		||||
            this.uiConfig = getConfigForUser(session.user);
 | 
			
		||||
 | 
			
		||||
        me().then((me: SessionUser) => {
 | 
			
		||||
            this.me = me;
 | 
			
		||||
            new EventsApi(DEFAULT_CONFIG)
 | 
			
		||||
                .eventsNotificationsList({
 | 
			
		||||
                    seen: false,
 | 
			
		||||
@ -338,16 +334,12 @@ export class UserInterface extends AuthenticatedInterface {
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render() {
 | 
			
		||||
        if (!this.me) {
 | 
			
		||||
            console.debug(`authentik/user/UserInterface: waiting for user session to be available`);
 | 
			
		||||
 | 
			
		||||
            return nothing;
 | 
			
		||||
    get isFullyConfigured() {
 | 
			
		||||
        return Boolean(this.uiConfig && this.me);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        if (!this.uiConfig) {
 | 
			
		||||
            console.debug(`authentik/user/UserInterface: waiting for UI config to be available`);
 | 
			
		||||
 | 
			
		||||
    render() {
 | 
			
		||||
        if (!this.isFullyConfigured) {
 | 
			
		||||
            return nothing;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										2106
									
								
								website/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2106
									
								
								website/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -33,7 +33,7 @@
 | 
			
		||||
        "docusaurus-theme-openapi-docs": "4.3.4",
 | 
			
		||||
        "postcss": "^8.5.3",
 | 
			
		||||
        "prism-react-renderer": "^2.4.1",
 | 
			
		||||
        "react": "^18.3.1",
 | 
			
		||||
        "react": "^19.1.0",
 | 
			
		||||
        "react-before-after-slider-component": "^1.1.8",
 | 
			
		||||
        "react-dom": "^18.3.1",
 | 
			
		||||
        "react-feather": "^2.0.10",
 | 
			
		||||
@ -58,7 +58,7 @@
 | 
			
		||||
        "@docusaurus/module-type-aliases": "^3.3.2",
 | 
			
		||||
        "@docusaurus/tsconfig": "^3.7.0",
 | 
			
		||||
        "@docusaurus/types": "^3.3.2",
 | 
			
		||||
        "@types/react": "^18.3.13",
 | 
			
		||||
        "@types/react": "^19.1.2",
 | 
			
		||||
        "cross-env": "^7.0.3",
 | 
			
		||||
        "prettier": "3.5.3",
 | 
			
		||||
        "typescript": "~5.8.3",
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user