Compare commits

..

8 Commits

Author SHA1 Message Date
42a99e3672 add api
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-06-30 21:31:16 +02:00
4e501f2fbf better endpoint
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-06-30 21:31:16 +02:00
1cca629464 fix session
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-06-30 21:31:16 +02:00
4efdc3113e give session
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-06-30 21:31:16 +02:00
5a9b0f7b7a it works
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-06-30 21:31:16 +02:00
395ccc5af1 format
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-06-30 21:31:16 +02:00
c8ac4fcdd6 snap
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-06-30 21:31:15 +02:00
53c36394e9 init
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-06-30 21:31:14 +02:00
741 changed files with 6739 additions and 19006 deletions

View File

@ -78,13 +78,13 @@ updates:
patterns:
- "@goauthentik/*"
- package-ecosystem: npm
directory: "/docs"
directory: "/website"
schedule:
interval: daily
time: "04:00"
open-pull-requests-limit: 10
commit-message:
prefix: "docs:"
prefix: "website:"
labels:
- dependencies
groups:

View File

@ -31,4 +31,4 @@ If changes to the frontend have been made
If applicable
- [ ] The documentation has been updated
- [ ] The documentation has been formatted (`make docs`)
- [ ] The documentation has been formatted (`make website`)

View File

@ -1,83 +0,0 @@
name: authentik-ci-api-docs
on:
push:
branches:
- main
- next
- version-*
pull_request:
branches:
- main
- version-*
jobs:
lint:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
command:
- prettier-check
steps:
- uses: actions/checkout@v4
- name: Install Dependencies
working-directory: docs/
run: npm ci
- name: Lint
working-directory: docs/
run: npm run ${{ matrix.command }}
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: docs/package.json
cache: "npm"
cache-dependency-path: docs/package-lock.json
- working-directory: docs/
name: Install Dependencies
run: npm ci
- name: Build API Docs via Docusaurus
working-directory: docs
run: npm run build -w api
- uses: actions/upload-artifact@v4
with:
name: api-docs
path: docs/api/build
deploy:
runs-on: ubuntu-latest
needs:
- lint
- build
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: api-docs
path: docs/api/build
- uses: actions/setup-node@v4
with:
node-version-file: docs/package.json
cache: "npm"
cache-dependency-path: docs/package-lock.json
- working-directory: docs/
name: Install Dependencies
run: npm ci
- name: Deploy Netlify (Production)
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
env:
NETLIFY_SITE_ID: authentik-api-docs.netlify.app
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
working-directory: docs/api
run: npx netlify deploy --no-build --prod
- name: Deploy Netlify (Preview)
if: github.event_name == 'pull_request' || github.ref != 'refs/heads/main'
env:
NETLIFY_SITE_ID: authentik-api-docs.netlify.app
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
working-directory: docs/api
run: npx netlify deploy --no-build --alias=deploy-preview-${{ github.event.number }}

View File

@ -24,8 +24,8 @@ jobs:
run: |
# Create folder structure for go embeds
mkdir -p web/dist
mkdir -p docs/help
touch web/dist/test docs/help/test
mkdir -p website/help
touch web/dist/test website/help/test
- name: Generate API
run: make gen-client-go
- name: golangci-lint

View File

@ -1,4 +1,4 @@
name: authentik-ci-docs
name: authentik-ci-website
on:
push:
@ -18,47 +18,50 @@ jobs:
fail-fast: false
matrix:
command:
- lint:lockfile
- prettier-check
steps:
- uses: actions/checkout@v4
- name: Install dependencies
working-directory: docs/
- working-directory: website/
run: npm ci
- name: Lint
working-directory: docs/
working-directory: website/
run: npm run ${{ matrix.command }}
build-topics:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: docs/package.json
node-version-file: website/package.json
cache: "npm"
cache-dependency-path: docs/package-lock.json
- working-directory: docs/
name: Install Dependencies
cache-dependency-path: website/package-lock.json
- working-directory: website/
run: npm ci
- name: Build Documentation via Docusaurus
working-directory: docs/
run: npm run build
build-integrations:
- name: test
working-directory: website/
run: npm test
build:
runs-on: ubuntu-latest
name: ${{ matrix.job }}
strategy:
fail-fast: false
matrix:
job:
- build
- build:integrations
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: docs/package.json
node-version-file: website/package.json
cache: "npm"
cache-dependency-path: docs/package-lock.json
- working-directory: docs/
name: Install Dependencies
cache-dependency-path: website/package-lock.json
- working-directory: website/
run: npm ci
- name: Build Integrations via Docusaurus
working-directory: docs/
run: npm run build -w integrations
- name: build
working-directory: website/
run: npm run ${{ matrix.job }}
build-container:
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
runs-on: ubuntu-latest
@ -95,7 +98,7 @@ jobs:
uses: docker/build-push-action@v6
with:
tags: ${{ steps.ev.outputs.imageTags }}
file: docs/Dockerfile
file: website/Dockerfile
push: ${{ steps.ev.outputs.shouldPush == 'true' }}
platforms: linux/amd64,linux/arm64
context: .
@ -108,12 +111,12 @@ jobs:
subject-name: ${{ steps.ev.outputs.attestImageNames }}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true
ci-docs-mark:
ci-website-mark:
if: always()
needs:
- lint
- build-topics
- build-integrations
- test
- build
- build-container
runs-on: ubuntu-latest
steps:

View File

@ -52,7 +52,7 @@ jobs:
uses: docker/build-push-action@v6
with:
tags: ${{ steps.ev.outputs.imageTags }}
file: docs/Dockerfile
file: website/Dockerfile
push: true
platforms: linux/amd64,linux/arm64
context: .

View File

@ -10,7 +10,7 @@ coverage
dist
out
.docusaurus
docs/api/reference
website/docs/developer-docs/api/**/*
## Environment
*.env

44
.vscode/tasks.json vendored
View File

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

View File

@ -32,8 +32,8 @@ tests/wdio/ @goauthentik/frontend
locale/ @goauthentik/backend @goauthentik/frontend
web/xliff/ @goauthentik/backend @goauthentik/frontend
# Docs & Website
docs/ @goauthentik/docs
website/ @goauthentik/docs
CODE_OF_CONDUCT.md @goauthentik/docs
# Security
SECURITY.md @goauthentik/security @goauthentik/docs
docs/security/ @goauthentik/security @goauthentik/docs
website/docs/security/ @goauthentik/security @goauthentik/docs

View File

@ -18,7 +18,7 @@ RUN --mount=type=bind,target=/work/web/package.json,src=./web/package.json \
COPY ./package.json /work
COPY ./web /work/web/
COPY ./docs /work/docs/
COPY ./website /work/website/
COPY ./gen-ts-api /work/web/node_modules/@goauthentik/api
RUN npm run build && \

View File

@ -1,7 +1,7 @@
Copyright (c) 2023 Jens Langhammer
Portions of this software are licensed as follows:
* All content residing under the "docs/" directory of this repository is licensed under "Creative Commons: CC BY-SA 4.0 license".
* All content residing under the "website/" directory of this repository is licensed under "Creative Commons: CC BY-SA 4.0 license".
* All content that resides under the "authentik/enterprise/" directory of this repository, if that directory exists, is licensed under the license defined in "authentik/enterprise/LICENSE".
* All client-side JavaScript (when served directly or after being compiled, arranged, augmented, or combined), is licensed under the "MIT Expat" license.
* All third party components incorporated into the authentik are licensed under the original license provided by the owner of the applicable component.

View File

@ -1,4 +1,4 @@
.PHONY: gen dev-reset all clean test web docs
.PHONY: gen dev-reset all clean test web website
SHELL := /usr/bin/env bash
.SHELLFLAGS += ${SHELLFLAGS} -e -o pipefail
@ -70,10 +70,10 @@ core-i18n-extract:
--ignore internal \
--ignore ${GEN_API_TS} \
--ignore ${GEN_API_GO} \
--ignore docs \
--ignore website \
-l en
install: web-install docs-install core-install ## Install all requires dependencies for `web`, `docs` and `core`
install: web-install website-install core-install ## Install all requires dependencies for `web`, `website` and `core`
dev-drop-db:
dropdb -U ${pg_user} -h ${pg_host} ${pg_name}
@ -221,22 +221,22 @@ web-i18n-extract:
cd web && npm run extract-locales
#########################
## Docs
## Website
#########################
docs: docs-lint-fix docs-build ## Automatically fix formatting issues in the Authentik docs source code, lint the code, and compile it
website: website-lint-fix website-build ## Automatically fix formatting issues in the Authentik website/docs source code, lint the code, and compile it
docs-install:
npm ci --prefix docs
website-install:
cd website && npm ci
docs-lint-fix: lint-codespell
npm run prettier --prefix docs
website-lint-fix: lint-codespell
cd website && npm run prettier
docs-build:
npm run build --prefix docs
website-build:
cd website && npm run build
docs-watch: ## Build and watch the documentation website, updating automatically
npm run watch --prefix docs
website-watch: ## Build and watch the documentation website, updating automatically
cd website && npm run watch
#########################
## Docker

View File

@ -43,6 +43,7 @@ from authentik.core.models import (
)
from authentik.enterprise.license import LicenseKey
from authentik.enterprise.models import LicenseUsage
from authentik.enterprise.providers.apple_psso.models import AppleNonce
from authentik.enterprise.providers.google_workspace.models import (
GoogleWorkspaceProviderGroup,
GoogleWorkspaceProviderUser,
@ -135,6 +136,7 @@ def excluded_models() -> list[type[Model]]:
EndpointDeviceConnection,
DeviceToken,
StreamEvent,
AppleNonce,
)

View File

@ -0,0 +1,32 @@
"""Apple Platform SSO Provider API Views"""
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.providers import ProviderSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.enterprise.api import EnterpriseRequiredMixin
from authentik.enterprise.providers.apple_psso.models import ApplePlatformSSOProvider
class ApplePlatformSSOProviderSerializer(EnterpriseRequiredMixin, ProviderSerializer):
"""ApplePlatformSSOProvider Serializer"""
class Meta:
model = ApplePlatformSSOProvider
fields = [
"pk",
"name",
]
extra_kwargs = {}
class ApplePlatformSSOProviderViewSet(UsedByMixin, ModelViewSet):
"""ApplePlatformSSOProvider Viewset"""
queryset = ApplePlatformSSOProvider.objects.all()
serializer_class = ApplePlatformSSOProviderSerializer
filterset_fields = [
"name",
]
search_fields = ["name"]
ordering = ["name"]

View File

@ -0,0 +1,13 @@
from authentik.enterprise.apps import EnterpriseConfig
class AuthentikEnterpriseProviderApplePSSOConfig(EnterpriseConfig):
name = "authentik.enterprise.providers.apple_psso"
label = "authentik_providers_apple_psso"
verbose_name = "authentik Enterprise.Providers.Apple Platform SSO"
default = True
mountpoints = {
"authentik.enterprise.providers.apple_psso.urls": "endpoint/apple/sso/",
"authentik.enterprise.providers.apple_psso.urls_root": "",
}

View File

@ -0,0 +1,118 @@
from base64 import urlsafe_b64encode
from json import dumps
from secrets import token_bytes
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives.kdf.concatkdf import ConcatKDFHash
from django.http import HttpResponse
from jwcrypto.common import base64url_decode, base64url_encode
from authentik.enterprise.providers.apple_psso.models import AppleDevice
def length_prefixed(data: bytes) -> bytes:
length = len(data)
return length.to_bytes(4, "big") + data
def build_apu(public_key: ec.EllipticCurvePublicKey):
# X9.63 representation: 0x04 || X || Y
public_numbers = public_key.public_numbers()
x_bytes = public_numbers.x.to_bytes(32, "big")
y_bytes = public_numbers.y.to_bytes(32, "big")
x963 = bytes([0x04]) + x_bytes + y_bytes
result = length_prefixed(b"APPLE") + length_prefixed(x963)
return result
def encrypt_token_with_a256_gcm(body: dict, device_encryption_key: str, apv: bytes) -> str:
ephemeral_key = ec.generate_private_key(curve=ec.SECP256R1())
device_public_key = serialization.load_pem_public_key(
device_encryption_key.encode(), backend=default_backend()
)
shared_secret_z = ephemeral_key.exchange(ec.ECDH(), device_public_key)
apu = build_apu(ephemeral_key.public_key())
jwe_header = {
"enc": "A256GCM",
"kid": "ephemeralKey",
"epk": {
"x": base64url_encode(
ephemeral_key.public_key().public_numbers().x.to_bytes(32, "big")
),
"y": base64url_encode(
ephemeral_key.public_key().public_numbers().y.to_bytes(32, "big")
),
"kty": "EC",
"crv": "P-256",
},
"typ": "platformsso-login-response+jwt",
"alg": "ECDH-ES",
"apu": base64url_encode(apu),
"apv": base64url_encode(apv),
}
party_u_info = length_prefixed(apu)
party_v_info = length_prefixed(apv)
supp_pub_info = (256).to_bytes(4, "big")
other_info = length_prefixed(b"A256GCM") + party_u_info + party_v_info + supp_pub_info
ckdf = ConcatKDFHash(
algorithm=hashes.SHA256(),
length=32,
otherinfo=other_info,
)
derived_key = ckdf.derive(shared_secret_z)
nonce = token_bytes(12)
header_json = dumps(jwe_header, separators=(",", ":")).encode()
aad = urlsafe_b64encode(header_json).rstrip(b"=")
aesgcm = AESGCM(derived_key)
ciphertext = aesgcm.encrypt(nonce, dumps(body).encode(), aad)
ciphertext_body = ciphertext[:-16]
tag = ciphertext[-16:]
# base64url encoding
protected_b64 = urlsafe_b64encode(header_json).rstrip(b"=")
iv_b64 = urlsafe_b64encode(nonce).rstrip(b"=")
ciphertext_b64 = urlsafe_b64encode(ciphertext_body).rstrip(b"=")
tag_b64 = urlsafe_b64encode(tag).rstrip(b"=")
jwe_compact = b".".join(
[
protected_b64,
b"",
iv_b64,
ciphertext_b64,
tag_b64,
]
)
return jwe_compact.decode()
class JWEResponse(HttpResponse):
def __init__(
self,
data: dict,
device: AppleDevice,
apv: str,
):
super().__init__(
content=encrypt_token_with_a256_gcm(data, device.encryption_key, base64url_decode(apv)),
content_type="application/platformsso-login-response+jwt",
)

View File

@ -0,0 +1,36 @@
# Generated by Django 5.1.11 on 2025-06-28 00:12
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("authentik_providers_oauth2", "0028_migrate_session"),
]
operations = [
migrations.CreateModel(
name="ApplePlatformSSOProvider",
fields=[
(
"oauth2provider_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="authentik_providers_oauth2.oauth2provider",
),
),
],
options={
"abstract": False,
},
bases=("authentik_providers_oauth2.oauth2provider",),
),
]

View File

@ -0,0 +1,94 @@
# Generated by Django 5.1.11 on 2025-06-28 15:50
import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_providers_apple_psso", "0001_initial"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="AppleDevice",
fields=[
(
"endpoint_uuid",
models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False),
),
("signing_key", models.TextField()),
("encryption_key", models.TextField()),
("key_exchange_key", models.TextField()),
("sign_key_id", models.TextField()),
("enc_key_id", models.TextField()),
("creation_time", models.DateTimeField(auto_now_add=True)),
(
"provider",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="authentik_providers_apple_psso.appleplatformssoprovider",
),
),
],
),
migrations.CreateModel(
name="AppleDeviceUser",
fields=[
("uuid", models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
("signing_key", models.TextField()),
("encryption_key", models.TextField()),
("sign_key_id", models.TextField()),
("enc_key_id", models.TextField()),
(
"device",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="authentik_providers_apple_psso.appledevice",
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
),
),
],
),
migrations.AddField(
model_name="appledevice",
name="users",
field=models.ManyToManyField(
through="authentik_providers_apple_psso.AppleDeviceUser",
to=settings.AUTH_USER_MODEL,
),
),
migrations.CreateModel(
name="AppleNonce",
fields=[
(
"id",
models.AutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
("expires", models.DateTimeField(default=None, null=True)),
("expiring", models.BooleanField(default=True)),
("nonce", models.TextField()),
],
options={
"abstract": False,
"indexes": [
models.Index(fields=["expires"], name="authentik_p_expires_47d534_idx"),
models.Index(fields=["expiring"], name="authentik_p_expirin_87253e_idx"),
models.Index(
fields=["expiring", "expires"], name="authentik_p_expirin_20a7c9_idx"
),
],
},
),
]

View File

@ -0,0 +1,34 @@
# Generated by Django 5.1.11 on 2025-06-28 22:18
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
(
"authentik_providers_apple_psso",
"0002_appledevice_appledeviceuser_appledevice_users_and_more",
),
]
operations = [
migrations.RenameField(
model_name="appledeviceuser",
old_name="sign_key_id",
new_name="enclave_key_id",
),
migrations.RenameField(
model_name="appledeviceuser",
old_name="signing_key",
new_name="secure_enclave_key",
),
migrations.RemoveField(
model_name="appledeviceuser",
name="enc_key_id",
),
migrations.RemoveField(
model_name="appledeviceuser",
name="encryption_key",
),
]

View File

@ -0,0 +1,85 @@
from uuid import uuid4
from django.db import models
from django.utils.translation import gettext_lazy as _
from rest_framework.serializers import Serializer
from authentik.core.models import ExpiringModel, User
from authentik.crypto.models import CertificateKeyPair
from authentik.providers.oauth2.models import (
ClientTypes,
IssuerMode,
OAuth2Provider,
RedirectURI,
RedirectURIMatchingMode,
ScopeMapping,
)
class ApplePlatformSSOProvider(OAuth2Provider):
"""Integrate with Apple Platform SSO"""
def set_oauth_defaults(self):
"""Ensure all OAuth2-related settings are correct"""
self.issuer_mode = IssuerMode.PER_PROVIDER
self.client_type = ClientTypes.PUBLIC
self.signing_key = CertificateKeyPair.objects.get(name="authentik Self-signed Certificate")
self.include_claims_in_id_token = True
scopes = ScopeMapping.objects.filter(
managed__in=[
"goauthentik.io/providers/oauth2/scope-openid",
"goauthentik.io/providers/oauth2/scope-profile",
"goauthentik.io/providers/oauth2/scope-email",
"goauthentik.io/providers/oauth2/scope-offline_access",
"goauthentik.io/providers/oauth2/scope-authentik_api",
]
)
self.property_mappings.add(*list(scopes))
self.redirect_uris = [
RedirectURI(RedirectURIMatchingMode.STRICT, "io.goauthentik.endpoint:/oauth2redirect"),
]
@property
def component(self) -> str:
return "ak-provider-apple-psso-form"
@property
def serializer(self) -> type[Serializer]:
from authentik.enterprise.providers.apple_psso.api.providers import (
ApplePlatformSSOProviderSerializer,
)
return ApplePlatformSSOProviderSerializer
class Meta:
verbose_name = _("Apple Platform SSO Provider")
verbose_name_plural = _("Apple Platform SSO Providers")
class AppleDevice(models.Model):
endpoint_uuid = models.UUIDField(default=uuid4, primary_key=True)
signing_key = models.TextField()
encryption_key = models.TextField()
key_exchange_key = models.TextField()
sign_key_id = models.TextField()
enc_key_id = models.TextField()
creation_time = models.DateTimeField(auto_now_add=True)
provider = models.ForeignKey(ApplePlatformSSOProvider, on_delete=models.CASCADE)
users = models.ManyToManyField(User, through="AppleDeviceUser")
class AppleDeviceUser(models.Model):
uuid = models.UUIDField(default=uuid4, primary_key=True)
device = models.ForeignKey(AppleDevice, on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.CASCADE)
secure_enclave_key = models.TextField()
enclave_key_id = models.TextField()
class AppleNonce(ExpiringModel):
nonce = models.TextField()

View File

@ -0,0 +1,15 @@
from django.urls import path
from authentik.enterprise.providers.apple_psso.views.nonce import NonceView
from authentik.enterprise.providers.apple_psso.views.register import (
RegisterDeviceView,
RegisterUserView,
)
from authentik.enterprise.providers.apple_psso.views.token import TokenView
urlpatterns = [
path("token/", TokenView.as_view(), name="token"),
path("nonce/", NonceView.as_view(), name="nonce"),
path("register/device/", RegisterDeviceView.as_view(), name="register-device"),
path("register/user/", RegisterUserView.as_view(), name="register-user"),
]

View File

@ -0,0 +1,7 @@
from django.urls import path
from authentik.enterprise.providers.apple_psso.views.site_association import AppleAppSiteAssociation
urlpatterns = [
path(".well-known/apple-app-site-association", AppleAppSiteAssociation.as_view(), name="asa"),
]

View File

@ -0,0 +1,25 @@
from base64 import b64encode
from datetime import timedelta
from secrets import token_bytes
from django.http import HttpRequest, JsonResponse
from django.utils.decorators import method_decorator
from django.utils.timezone import now
from django.views import View
from django.views.decorators.csrf import csrf_exempt
from authentik.enterprise.providers.apple_psso.models import AppleNonce
@method_decorator(csrf_exempt, name="dispatch")
class NonceView(View):
def post(self, request: HttpRequest, *args, **kwargs):
nonce = AppleNonce.objects.create(
nonce=b64encode(token_bytes(32)).decode(), expires=now() + timedelta(minutes=5)
)
return JsonResponse(
{
"Nonce": nonce.nonce,
}
)

View File

@ -0,0 +1,92 @@
from django.shortcuts import get_object_or_404
from rest_framework.authentication import BaseAuthentication
from rest_framework.fields import CharField
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.views import APIView
from authentik.api.authentication import TokenAuthentication
from authentik.core.api.utils import PassiveSerializer
from authentik.core.models import User
from authentik.enterprise.providers.apple_psso.models import (
AppleDevice,
AppleDeviceUser,
ApplePlatformSSOProvider,
)
from authentik.lib.generators import generate_key
class DeviceRegisterAuth(BaseAuthentication):
def authenticate(self, request):
# very temporary, lol
return (User(), None)
class RegisterDeviceView(APIView):
class DeviceRegistration(PassiveSerializer):
device_uuid = CharField()
client_id = CharField()
device_signing_key = CharField()
device_encryption_key = CharField()
sign_key_id = CharField()
enc_key_id = CharField()
permission_classes = []
pagination_class = None
filter_backends = []
serializer_class = DeviceRegistration
authentication_classes = [DeviceRegisterAuth, TokenAuthentication]
def post(self, request: Request) -> Response:
data = self.DeviceRegistration(data=request.data)
data.is_valid(raise_exception=True)
provider = get_object_or_404(
ApplePlatformSSOProvider, client_id=data.validated_data["client_id"]
)
AppleDevice.objects.update_or_create(
endpoint_uuid=data.validated_data["device_uuid"],
defaults={
"signing_key": data.validated_data["device_signing_key"],
"encryption_key": data.validated_data["device_encryption_key"],
"sign_key_id": data.validated_data["sign_key_id"],
"enc_key_id": data.validated_data["enc_key_id"],
"key_exchange_key": generate_key(),
"provider": provider,
},
)
return Response()
class RegisterUserView(APIView):
class UserRegistration(PassiveSerializer):
device_uuid = CharField()
user_secure_enclave_key = CharField()
enclave_key_id = CharField()
permission_classes = []
pagination_class = None
filter_backends = []
serializer_class = UserRegistration
authentication_classes = [TokenAuthentication]
def post(self, request: Request) -> Response:
data = self.UserRegistration(data=request.data)
data.is_valid(raise_exception=True)
device = get_object_or_404(AppleDevice, endpoint_uuid=data.validated_data["device_uuid"])
AppleDeviceUser.objects.update_or_create(
device=device,
user=request.user,
defaults={
"secure_enclave_key": data.validated_data["user_secure_enclave_key"],
"enclave_key_id": data.validated_data["enclave_key_id"],
},
)
return Response(
{
"username": request.user.username,
}
)

View File

@ -0,0 +1,16 @@
from django.http import HttpRequest, HttpResponse, JsonResponse
from django.views import View
class AppleAppSiteAssociation(View):
def get(self, request: HttpRequest) -> HttpResponse:
return JsonResponse(
{
"authsrv": {
"apps": [
"232G855Y8N.io.goauthentik.endpoint",
"232G855Y8N.io.goauthentik.endpoint.psso",
]
}
}
)

View File

@ -0,0 +1,140 @@
from datetime import timedelta
from django.http import Http404, HttpRequest, HttpResponse
from django.utils.decorators import method_decorator
from django.utils.timezone import now
from django.views import View
from django.views.decorators.csrf import csrf_exempt
from jwt import PyJWT, decode
from rest_framework.exceptions import ValidationError
from structlog.stdlib import get_logger
from authentik.core.models import AuthenticatedSession, Session, User
from authentik.core.sessions import SessionStore
from authentik.enterprise.providers.apple_psso.http import JWEResponse
from authentik.enterprise.providers.apple_psso.models import (
AppleDevice,
AppleDeviceUser,
AppleNonce,
ApplePlatformSSOProvider,
)
from authentik.events.models import Event, EventAction
from authentik.events.signals import SESSION_LOGIN_EVENT
from authentik.providers.oauth2.constants import TOKEN_TYPE
from authentik.providers.oauth2.id_token import IDToken
from authentik.providers.oauth2.models import RefreshToken
from authentik.root.middleware import SessionMiddleware
LOGGER = get_logger()
@method_decorator(csrf_exempt, name="dispatch")
class TokenView(View):
device: AppleDevice
provider: ApplePlatformSSOProvider
def post(self, request: HttpRequest) -> HttpResponse:
version = request.POST.get("platform_sso_version")
assertion = request.POST.get("assertion", request.POST.get("request"))
if not assertion:
return HttpResponse(status=400)
decode_unvalidated = PyJWT().decode_complete(assertion, options={"verify_signature": False})
LOGGER.debug(decode_unvalidated["header"])
expected_kid = decode_unvalidated["header"]["kid"]
self.device = AppleDevice.objects.filter(sign_key_id=expected_kid).first()
if not self.device:
raise Http404
self.provider = self.device.provider
# Properly decode the JWT with the key from the device
decoded = decode(
assertion, self.device.signing_key, algorithms=["ES256"], options={"verify_aud": False}
)
LOGGER.debug(decoded)
LOGGER.debug("got device", device=self.device)
# Check that the nonce hasn't been used before
nonce = AppleNonce.objects.filter(nonce=decoded["request_nonce"]).first()
if not nonce:
return HttpResponse(status=400)
nonce.delete()
handler_func = (
f"handle_v{version}_{decode_unvalidated["header"]["typ"]}".replace("-", "_")
.replace("+", "_")
.replace(".", "_")
)
handler = getattr(self, handler_func, None)
if not handler:
LOGGER.debug("Handler not found", handler=handler_func)
return HttpResponse(status=400)
LOGGER.debug("sending to handler", handler=handler_func)
return handler(decoded)
def validate_device_user_response(self, assertion: str) -> tuple[AppleDeviceUser, dict] | None:
"""Decode an embedded assertion and validate it by looking up the matching device user"""
decode_unvalidated = PyJWT().decode_complete(assertion, options={"verify_signature": False})
expected_kid = decode_unvalidated["header"]["kid"]
device_user = AppleDeviceUser.objects.filter(
device=self.device, enclave_key_id=expected_kid
).first()
if not device_user:
return None
return device_user, decode(
assertion,
device_user.secure_enclave_key,
audience="apple-platform-sso",
algorithms=["ES256"],
)
def create_auth_session(self, user: User):
event = Event.new(EventAction.LOGIN).from_http(self.request, user=user)
store = SessionStore()
store[SESSION_LOGIN_EVENT] = event
store.save()
session = Session.objects.filter(session_key=store.session_key).first()
AuthenticatedSession.objects.create(session=session, user=user)
session = SessionMiddleware.encode_session(store.session_key, user)
return session
def handle_v1_0_platformsso_login_request_jwt(self, decoded: dict):
user = None
if decoded["grant_type"] == "urn:ietf:params:oauth:grant-type:jwt-bearer":
# Decode and validate inner assertion
user, inner = self.validate_device_user_response(decoded["assertion"])
if inner["nonce"] != decoded["nonce"]:
LOGGER.warning("Mis-matched nonce to outer assertion")
raise ValidationError("Invalid request")
refresh_token = RefreshToken(
user=user.user,
scope=decoded["scope"],
expires=now() + timedelta(hours=8),
provider=self.provider,
auth_time=now(),
session=None,
)
id_token = IDToken.new(
self.provider,
refresh_token,
self.request,
)
id_token.nonce = decoded["nonce"]
refresh_token.id_token = id_token
refresh_token.save()
return JWEResponse(
{
"refresh_token": refresh_token.token,
"refresh_token_expires_in": int((refresh_token.expires - now()).total_seconds()),
"id_token": refresh_token.id_token.to_jwt(self.provider),
"token_type": TOKEN_TYPE,
"session_key": self.create_auth_session(user.user),
},
device=self.device,
apv=decoded["jwe_crypto"]["apv"],
)

View File

@ -6,7 +6,7 @@ from djangoql.ast import Name
from djangoql.exceptions import DjangoQLError
from djangoql.queryset import apply_search
from djangoql.schema import DjangoQLSchema
from rest_framework.filters import BaseFilterBackend, SearchFilter
from rest_framework.filters import SearchFilter
from rest_framework.request import Request
from structlog.stdlib import get_logger
@ -39,21 +39,19 @@ class BaseSchema(DjangoQLSchema):
return super().resolve_name(name)
class QLSearch(BaseFilterBackend):
class QLSearch(SearchFilter):
"""rest_framework search filter which uses DjangoQL"""
def __init__(self):
super().__init__()
self._fallback = SearchFilter()
@property
def enabled(self):
return apps.get_app_config("authentik_enterprise").enabled()
def get_search_terms(self, request: Request) -> str:
"""Search terms are set by a ?search=... query parameter,
and may be comma and/or whitespace delimited."""
params = request.query_params.get("search", "")
def get_search_terms(self, request) -> str:
"""
Search terms are set by a ?search=... query parameter,
and may be comma and/or whitespace delimited.
"""
params = request.query_params.get(self.search_param, "")
params = params.replace("\x00", "") # strip null characters
return params
@ -72,9 +70,9 @@ class QLSearch(BaseFilterBackend):
search_query = self.get_search_terms(request)
schema = self.get_schema(request, view)
if len(search_query) == 0 or not self.enabled:
return self._fallback.filter_queryset(request, queryset, view)
return super().filter_queryset(request, queryset, view)
try:
return apply_search(queryset, search_query, schema=schema)
except DjangoQLError as exc:
LOGGER.debug("Failed to parse search expression", exc=exc)
return self._fallback.filter_queryset(request, queryset, view)
return super().filter_queryset(request, queryset, view)

View File

@ -57,7 +57,7 @@ class QLTest(APITestCase):
)
self.assertEqual(res.status_code, 200)
content = loads(res.content)
self.assertEqual(content["pagination"]["count"], 1)
self.assertGreaterEqual(content["pagination"]["count"], 1)
self.assertEqual(content["results"][0]["username"], self.user.username)
def test_search_json(self):

View File

@ -15,6 +15,7 @@ CELERY_BEAT_SCHEDULE = {
TENANT_APPS = [
"authentik.enterprise.audit",
"authentik.enterprise.policies.unique_password",
"authentik.enterprise.providers.apple_psso",
"authentik.enterprise.providers.google_workspace",
"authentik.enterprise.providers.microsoft_entra",
"authentik.enterprise.providers.ssf",

View File

@ -8,12 +8,12 @@
# make gen-dev-config
# ```
#
# You may edit the generated file to override the configuration below.
# You may edit the generated file to override the configuration below.
#
# When making modifying the default configuration file,
# When making modifying the default configuration file,
# ensure that the corresponding documentation is updated to match.
#
# @see {@link ../../docs/topics/install-config/configuration/configuration.mdx Configuration documentation} for more information.
# @see {@link ../../website/docs/install-config/configuration/configuration.mdx Configuration documentation} for more information.
postgresql:
host: localhost

View File

@ -555,6 +555,8 @@ class TokenView(View):
provider: OAuth2Provider | None = None
params: TokenParams | None = None
params_class = TokenParams
provider_class = OAuth2Provider
def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
response = super().dispatch(request, *args, **kwargs)
@ -574,12 +576,14 @@ class TokenView(View):
op="authentik.providers.oauth2.post.parse",
):
client_id, client_secret = extract_client_auth(request)
self.provider = OAuth2Provider.objects.filter(client_id=client_id).first()
self.provider = self.provider_class.objects.filter(client_id=client_id).first()
if not self.provider:
LOGGER.warning("OAuth2Provider does not exist", client_id=client_id)
raise TokenError("invalid_client")
CTX_AUTH_VIA.set("oauth_client_secret")
self.params = TokenParams.parse(request, self.provider, client_id, client_secret)
self.params = self.params_class.parse(
request, self.provider, client_id, client_secret
)
with start_span(
op="authentik.providers.oauth2.post.response",

View File

@ -61,6 +61,22 @@ class SessionMiddleware(UpstreamSessionMiddleware):
pass
return session_key
@staticmethod
def encode_session(session_key: str, user: User):
payload = {
"sid": session_key,
"iss": "authentik",
"sub": "anonymous",
"authenticated": user.is_authenticated,
"acr": ACR_AUTHENTIK_SESSION,
}
if user.is_authenticated:
payload["sub"] = user.uid
value = encode(payload=payload, key=SIGNING_HASH)
if settings.TEST:
value = session_key
return value
def process_request(self, request: HttpRequest):
raw_session = request.COOKIES.get(settings.SESSION_COOKIE_NAME)
session_key = SessionMiddleware.decode_session_key(raw_session)
@ -117,21 +133,9 @@ class SessionMiddleware(UpstreamSessionMiddleware):
"request completed. The user may have logged "
"out in a concurrent request, for example."
) from None
payload = {
"sid": request.session.session_key,
"iss": "authentik",
"sub": "anonymous",
"authenticated": request.user.is_authenticated,
"acr": ACR_AUTHENTIK_SESSION,
}
if request.user.is_authenticated:
payload["sub"] = request.user.uid
value = encode(payload=payload, key=SIGNING_HASH)
if settings.TEST:
value = request.session.session_key
response.set_cookie(
settings.SESSION_COOKIE_NAME,
value,
SessionMiddleware.encode_session(request.session.session_key, request.user),
max_age=max_age,
expires=expires,
domain=settings.SESSION_COOKIE_DOMAIN,

File diff suppressed because one or more lines are too long

View File

@ -496,6 +496,46 @@
}
}
},
{
"type": "object",
"required": [
"model",
"identifiers"
],
"properties": {
"model": {
"const": "authentik_providers_apple_psso.appleplatformssoprovider"
},
"id": {
"type": "string"
},
"state": {
"type": "string",
"enum": [
"absent",
"created",
"must_created",
"present"
],
"default": "present"
},
"conditions": {
"type": "array",
"items": {
"type": "boolean"
}
},
"permissions": {
"$ref": "#/$defs/model_authentik_providers_apple_psso.appleplatformssoprovider_permissions"
},
"attrs": {
"$ref": "#/$defs/model_authentik_providers_apple_psso.appleplatformssoprovider"
},
"identifiers": {
"$ref": "#/$defs/model_authentik_providers_apple_psso.appleplatformssoprovider"
}
}
},
{
"type": "object",
"required": [
@ -5028,6 +5068,22 @@
"authentik_policies_unique_password.delete_userpasswordhistory",
"authentik_policies_unique_password.view_uniquepasswordpolicy",
"authentik_policies_unique_password.view_userpasswordhistory",
"authentik_providers_apple_psso.add_appledevice",
"authentik_providers_apple_psso.add_appledeviceuser",
"authentik_providers_apple_psso.add_applenonce",
"authentik_providers_apple_psso.add_appleplatformssoprovider",
"authentik_providers_apple_psso.change_appledevice",
"authentik_providers_apple_psso.change_appledeviceuser",
"authentik_providers_apple_psso.change_applenonce",
"authentik_providers_apple_psso.change_appleplatformssoprovider",
"authentik_providers_apple_psso.delete_appledevice",
"authentik_providers_apple_psso.delete_appledeviceuser",
"authentik_providers_apple_psso.delete_applenonce",
"authentik_providers_apple_psso.delete_appleplatformssoprovider",
"authentik_providers_apple_psso.view_appledevice",
"authentik_providers_apple_psso.view_appledeviceuser",
"authentik_providers_apple_psso.view_applenonce",
"authentik_providers_apple_psso.view_appleplatformssoprovider",
"authentik_providers_google_workspace.add_googleworkspaceprovider",
"authentik_providers_google_workspace.add_googleworkspaceprovidergroup",
"authentik_providers_google_workspace.add_googleworkspaceprovidermapping",
@ -5599,6 +5655,43 @@
}
}
},
"model_authentik_providers_apple_psso.appleplatformssoprovider": {
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 1,
"title": "Name"
}
},
"required": []
},
"model_authentik_providers_apple_psso.appleplatformssoprovider_permissions": {
"type": "array",
"items": {
"type": "object",
"required": [
"permission"
],
"properties": {
"permission": {
"type": "string",
"enum": [
"add_appleplatformssoprovider",
"change_appleplatformssoprovider",
"delete_appleplatformssoprovider",
"view_appleplatformssoprovider"
]
},
"user": {
"type": "integer"
},
"role": {
"type": "string"
}
}
}
},
"model_authentik_providers_google_workspace.googleworkspaceprovider": {
"type": "object",
"properties": {
@ -7342,6 +7435,7 @@
"authentik.enterprise",
"authentik.enterprise.audit",
"authentik.enterprise.policies.unique_password",
"authentik.enterprise.providers.apple_psso",
"authentik.enterprise.providers.google_workspace",
"authentik.enterprise.providers.microsoft_entra",
"authentik.enterprise.providers.ssf",
@ -7452,6 +7546,7 @@
"authentik_core.token",
"authentik_enterprise.license",
"authentik_policies_unique_password.uniquepasswordpolicy",
"authentik_providers_apple_psso.appleplatformssoprovider",
"authentik_providers_google_workspace.googleworkspaceprovider",
"authentik_providers_google_workspace.googleworkspaceprovidermapping",
"authentik_providers_microsoft_entra.microsoftentraprovider",
@ -9674,6 +9769,22 @@
"authentik_policies_unique_password.delete_userpasswordhistory",
"authentik_policies_unique_password.view_uniquepasswordpolicy",
"authentik_policies_unique_password.view_userpasswordhistory",
"authentik_providers_apple_psso.add_appledevice",
"authentik_providers_apple_psso.add_appledeviceuser",
"authentik_providers_apple_psso.add_applenonce",
"authentik_providers_apple_psso.add_appleplatformssoprovider",
"authentik_providers_apple_psso.change_appledevice",
"authentik_providers_apple_psso.change_appledeviceuser",
"authentik_providers_apple_psso.change_applenonce",
"authentik_providers_apple_psso.change_appleplatformssoprovider",
"authentik_providers_apple_psso.delete_appledevice",
"authentik_providers_apple_psso.delete_appledeviceuser",
"authentik_providers_apple_psso.delete_applenonce",
"authentik_providers_apple_psso.delete_appleplatformssoprovider",
"authentik_providers_apple_psso.view_appledevice",
"authentik_providers_apple_psso.view_appledeviceuser",
"authentik_providers_apple_psso.view_applenonce",
"authentik_providers_apple_psso.view_appleplatformssoprovider",
"authentik_providers_google_workspace.add_googleworkspaceprovider",
"authentik_providers_google_workspace.add_googleworkspaceprovidergroup",
"authentik_providers_google_workspace.add_googleworkspaceprovidermapping",

View File

@ -1,9 +0,0 @@
[production]
> 0.2%
not dead
not op_mini all
[development]
last 1 chrome version
last 1 firefox version
last 1 safari version

View File

@ -1,20 +0,0 @@
FROM --platform=${BUILDPLATFORM} docker.io/library/node:24-slim AS docs-builder
ENV NODE_ENV=production
WORKDIR /work/docs
COPY ./docs/package.json ./docs/package-lock.json /work/docs/
RUN npm ci --include=dev
COPY ./docs /work/docs/
COPY ./blueprints /work/blueprints/
COPY ./schema.yml /work/
COPY ./SECURITY.md /work/
RUN npm run build
FROM docker.io/library/nginx:1.29.0
COPY --from=docs-builder /work/docs/topics/build /usr/share/nginx/html

View File

@ -1,18 +0,0 @@
---
title: Authentication
sidebar_position: 1
---
For any of the token-based methods, set the `Authorization` header to `Bearer <token>`.
### Session
When authenticating with a flow, you'll get an authenticated Session cookie, that can be used for authentication. Keep in mind that in this context, a CSRF header is also required.
### API Token
Users can create tokens to authenticate as any user with a static key, which can optionally be expiring and auto-rotate.
### JWT Token
OAuth2 clients can request the scope `goauthentik.io/api`, which allows their OAuth Access token to be used to authenticate to the API.

View File

@ -1,15 +0,0 @@
---
title: API Client Overview
---
import DocCardList from "@theme/DocCardList";
These API clients are officially supported and maintained.
:::info
These API clients are primarily built around creating/updating/deleting configuration objects in authentik, and in most cases can **not** be used to implemented SSO into your application.
:::
<DocCardList />

View File

@ -1,17 +0,0 @@
---
title: Go API Client
sidebar_label: Golang
description: A Golang client for the authentik API.
---
The [Go API client](https://pkg.go.dev/goauthentik.io/api/v3) is generated using the [OpenAPI Generator](https://openapi-generator.tech/) and the [OpenAPI v3 schema](https://docs.goauthentik.io/schema.yml).
```bash
go get goauthentik.io/api/v3
```
## Building the Go Client
The Go client is used by the Outpost to communicate with the backend authentik server. To build the go client, run `make gen-client-go`.
The generated files are stored in `/gen-go-api` in the root of the repository.

View File

@ -1,33 +0,0 @@
---
title: Node.js API Client
sidebar_label: Node.js
description: A TypeScript client for the authentik API.
---
The [Node.js API client](https://www.npmjs.com/package/@goauthentik/api) is generated using the [OpenAPI Generator](https://openapi-generator.tech/) and the [OpenAPI v3 schema](https://docs.goauthentik.io/schema.yml).
```bash npm2yarn
npm install @goauthentik/api
```
## Usage
```ts
import { AdminApi, Configuration } from "@goauthentik/api";
const config = new Configuration({
basePath: "authentik.company/api/v3",
});
const status = await new AdminApi(DEFAULT_CONFIG).adminSystemRetrieve();
```
## Building the Node.js Client
The web client is used by the web-interface and web-FlowExecutor to communicate with authentik. To build the client, run `make gen-client-ts`.
Since the client is normally distributed as an npm package, running `make gen-client-ts` will overwrite the locally installed client with the newly built one.
:::caution
Running `npm i` in the `/web` folder after using `make gen-client-ts` will overwrite the custom client and revert to the upstream client.
:::

View File

@ -1,13 +0,0 @@
---
title: Python API Client
sidebar_label: Python
description: A Python client for the authentik API.
---
The [Python API client](https://pypi.org/project/authentik-client/) is generated using the [OpenAPI Generator](https://openapi-generator.tech/) and the [OpenAPI v3 schema](https://docs.goauthentik.io/schema.yml).
```bash
pip install authentik-client
# Or
uv pip install authentik-client
```

View File

@ -1 +0,0 @@
module.exports = import("./docusaurus.config.esm.mjs").then(($) => $.default);

View File

@ -1,161 +0,0 @@
/**
* @file Docusaurus config.
*
* @import { Config } from "@docusaurus/types";
* @import { UserThemeConfig, UserThemeConfigExtra } from "@goauthentik/docusaurus-config";
* @import { Options as DocsPluginOptions } from "@docusaurus/plugin-content-docs";
* @import * as OpenApiPlugin from "docusaurus-plugin-openapi-docs";
*/
import { createDocusaurusConfig } from "@goauthentik/docusaurus-config";
import { remarkLinkRewrite } from "@goauthentik/docusaurus-theme/remark";
import { GlobExcludeDefault } from "@docusaurus/utils";
import { createApiPageMD } from "docusaurus-plugin-openapi-docs/lib/markdown/index.js";
import { cp } from "node:fs/promises";
import { createRequire } from "node:module";
import { basename, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { gzip } from "pako";
const require = createRequire(import.meta.url);
const __dirname = fileURLToPath(new URL(".", import.meta.url));
const rootStaticDirectory = resolve(__dirname, "..", "static");
//#region Copy static files
const authentikModulePath = resolve(__dirname, "..", "..");
const files = [
resolve(authentikModulePath, "docker-compose.yml"),
resolve(authentikModulePath, "schema.yml"),
];
await Promise.all(
files.map((file) => {
const fileName = basename(file);
const destPath = resolve(rootStaticDirectory, fileName);
return cp(file, destPath, {
recursive: true,
});
}),
);
//#endregion
//#region Configuration
//#region Configuration
/**
* Documentation site configuration for Docusaurus.
* @satisfies {Partial<Config>}
*/
const config = {
staticDirectories: [
// ---
"static",
rootStaticDirectory,
],
onBrokenAnchors: "ignore",
onBrokenLinks: "ignore",
onBrokenMarkdownLinks: "ignore",
onDuplicateRoutes: "ignore",
themes: ["@docusaurus/theme-mermaid", "docusaurus-theme-openapi-docs"],
themeConfig: /** @type {UserThemeConfig & UserThemeConfigExtra} */ ({
navbarReplacements: {
DOCS_URL: "/",
},
docs: {
sidebar: {
hideable: true,
},
},
}),
plugins: [
[
"@docusaurus/theme-classic",
{
customCss: require.resolve("@goauthentik/docusaurus-config/css/index.css"),
},
],
//#region Docs Content Plugin
[
"@docusaurus/plugin-content-docs",
/** @type {DocsPluginOptions} */ ({
showLastUpdateAuthor: false,
showLastUpdateTime: false,
numberPrefixParser: false,
id: "docs",
routeBasePath: "/",
path: ".",
exclude: [...GlobExcludeDefault],
include: ["**/*.mdx", "**/*.md"],
sidebarPath: "./sidebar.mjs",
docItemComponent: "@theme/ApiItem",
beforeDefaultRemarkPlugins: [
remarkLinkRewrite([
// ---
["/integrations", "https://integrations.goauthentik.io"],
["/docs", "https://docs.goauthentik.io"],
]),
],
}),
],
//#endregion
//#region OpenAPI Docs Plugin
[
"docusaurus-plugin-openapi-docs",
{
id: "open-api-docs",
docsPluginId: "docs",
config: {
authentik: /** @type {OpenApiPlugin.Options} */ ({
specPath: resolve("..", "..", "schema.yml"),
outputDir: "./reference",
hideSendButton: true,
disableCompression: true,
sidebarOptions: {
groupPathsBy: "tag",
},
template: "src/templates/api.mustache",
markdownGenerators: {
createApiPageMD: (pageData) => {
const {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
info,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
postman,
...coreAPI
} = pageData.api;
return [
createApiPageMD(pageData),
`export const api = "${btoa(
String.fromCharCode(
...gzip(JSON.stringify(coreAPI), {
level: 9,
}),
),
)}";`,
].join("\n");
},
},
}),
},
},
],
],
//#endregion
};
//#endregion
export default createDocusaurusConfig(config);

View File

@ -1,11 +0,0 @@
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
try {
require.resolve("#reference/sidebar");
} catch (_error) {
console.error(
"\n⛔ API Reference sidebar not found.\n\nRun `npm run build:api` to generate files.",
);
process.exit(1);
}

View File

@ -1,16 +0,0 @@
---
title: API Overview
sidebar_label: Overview
---
Our API reference documentation is generated from the [OpenAPI v3 schema](https://docs.goauthentik.io/schema.yml).
You can also access your installation's own, instance-specific API Browser. Starting with 2021.3.5, every authentik instance has a built-in API browser, which can be accessed at <code>https://<em>authentik.company</em>/api/v3/</code>.
To generate an API client you can use the OpenAPI v3 schema at <code>https://<em>authentik.company</em>/api/v3/schema/</code>.
## Making schema changes
Some backend changes might require new/different fields or remove other fields. To create a new schema after changing a Serializer, run `make gen-build`.
This will update the `schema.yml` file in the root of the repository.

View File

@ -1,30 +0,0 @@
[[plugins]]
package = "netlify-plugin-cache"
[plugins.inputs]
paths = [".docusaurus", ".cache", 'node_modules/.cache']
[[plugins]]
package = "netlify-plugin-debug-cache"
[build]
base = "docs"
package = "api"
command = "npm run build -w api"
publish = "api/build"
[dev]
command = "npm start"
targetPort = 3000
publish = "api/build"
[context.production.environment]
NODE_ENV = "production"
[context.dev.environment]
NODE_ENV = "development"
[[headers]]
for = "/*"
[headers.values]
X-Frame-Options = "DENY"

View File

@ -1,24 +0,0 @@
{
"name": "@goauthentik/api-docs",
"version": "0.0.0",
"description": "API Documentation",
"license": "MIT",
"private": true,
"scripts": {
"build": "run-s build:api build:types build:docusaurus",
"build:api": "docusaurus gen-api-docs all",
"build:docusaurus": "docusaurus build",
"build:types": "tsc -b .",
"deploy": "docusaurus deploy",
"docusaurus": "docusaurus",
"serve": "docusaurus serve",
"start": "docusaurus start",
"swizzle": "docusaurus swizzle"
},
"imports": {
"#reference/sidebar": "./reference/sidebar.ts"
},
"dependencies": {
"@goauthentik/docusaurus-theme": "*"
}
}

View File

@ -1,65 +0,0 @@
/**
* @file Sidebar configuration for documentation entries.
*
* @import { SidebarItemConfig } from "@docusaurus/plugin-content-docs/src/sidebars/types.js"
*/
import "./ensure-reference-sidebar.mjs";
// No file extensions for Docusaurus's automatic resolution.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - Allows for project-wide type checking when partially building docs.
import apiReference from "./reference/sidebar";
const DOCS_URL = process.env.DOCS_URL || "https://docs.goauthentik.io";
/**
* @type {SidebarItemConfig}
*/
const sidebar = {
reference: [
{
type: "link",
label: "← Back to Developer Docs",
href: new URL("/developer-docs", DOCS_URL).href,
className: "navbar-sidebar__upwards",
},
{
type: "doc",
label: "API Overview",
className: "api-overview",
id: "index",
},
{
type: "category",
label: "Clients",
collapsed: false,
collapsible: false,
link: {
type: "doc",
id: "clients",
},
items: [
{
type: "autogenerated",
dirName: "clients",
},
],
},
{
type: "category",
label: "API Reference",
className: "api-reference",
collapsed: false,
collapsible: false,
link: {
type: "doc",
id: apiReference[0].id,
},
items: apiReference.slice(1),
},
],
};
export default sidebar;

View File

@ -1,25 +0,0 @@
---
id: {{{id}}}
title: "{{{title}}}"
description: "{{{frontMatter.description}}}"
{{^api}}
sidebar_label: Introduction
sidebar_position: 0
{{/api}}
hide_title: true
{{#api}}
hide_table_of_contents: true
{{/api}}
{{#json}}
api: true
{{/json}}
{{#api.method}}
sidebar_class_name: "{{{api.method}}} api-method"
{{/api.method}}
{{#infoPath}}
info_path: {{{infoPath}}}
{{/infoPath}}
hide_send_button: true
---
{{{markdown}}}

View File

@ -1,75 +0,0 @@
import { useDoc } from "@docusaurus/plugin-content-docs/client";
import { useWindowSize } from "@docusaurus/theme-common";
import type { Props } from "@theme/ApiItem/Layout";
import ContentVisibility from "@theme/ContentVisibility";
import DocBreadcrumbs from "@theme/DocBreadcrumbs";
import DocItemContent from "@theme/DocItem/Content";
import DocItemFooter from "@theme/DocItem/Footer";
import DocItemPaginator from "@theme/DocItem/Paginator";
import DocItemTOCDesktop from "@theme/DocItem/TOC/Desktop";
import DocItemTOCMobile from "@theme/DocItem/TOC/Mobile";
import DocVersionBadge from "@theme/DocVersionBadge";
import DocVersionBanner from "@theme/DocVersionBanner";
import clsx from "clsx";
import React, { type JSX } from "react";
import styles from "./styles.module.css";
/**
* Decide if the toc should be rendered, on mobile or desktop viewports
*/
function useDocTOC() {
const { frontMatter, toc } = useDoc();
const windowSize = useWindowSize();
const hidden = frontMatter.hide_table_of_contents;
const canRender = !hidden && toc.length > 0;
const mobile = canRender ? <DocItemTOCMobile /> : undefined;
const desktop =
canRender && (windowSize === "desktop" || windowSize === "ssr") ? (
<DocItemTOCDesktop />
) : undefined;
return {
hidden,
mobile,
desktop,
};
}
export default function DocItemLayout({ children }: Props): JSX.Element {
const docTOC = useDocTOC();
const { metadata, frontMatter } = useDoc() as DocContextValue;
const { api, schema } = frontMatter;
return (
<div className="row">
<div className={clsx("col", !docTOC.hidden && styles.docItemCol)}>
<ContentVisibility metadata={metadata} />
<DocVersionBanner />
<div className={styles.docItemContainer}>
<article>
<DocBreadcrumbs />
<DocVersionBadge />
{docTOC.mobile}
<DocItemContent>{children}</DocItemContent>
<div className="row">
<div className={clsx("col", api || schema ? "col--7" : "col--12")}>
<DocItemFooter />
</div>
</div>
</article>
<div className="row">
<div className={clsx("col", api || schema ? "col--7" : "col--12")}>
<DocItemPaginator />
</div>
</div>
</div>
</div>
{docTOC.desktop ? <div className="col col--3">{docTOC.desktop}</div> : null}
</div>
);
}

View File

@ -1,10 +0,0 @@
.docItemContainer header + *,
.docItemContainer article > *:first-child {
margin-top: 0;
}
@media (min-width: 997px) {
.docItemCol {
max-width: 75% !important;
}
}

View File

@ -1,245 +0,0 @@
import BrowserOnly from "@docusaurus/BrowserOnly";
import ExecutionEnvironment from "@docusaurus/ExecutionEnvironment";
import { DocProvider } from "@docusaurus/plugin-content-docs/client";
import { HtmlClassNameProvider } from "@docusaurus/theme-common";
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
import useIsBrowser from "@docusaurus/useIsBrowser";
import type { ApiExplorerProps } from "@theme/APIExplorer";
import { createAuth } from "@theme/ApiExplorer/Authorization/slice";
import { createPersistanceMiddleware } from "@theme/ApiExplorer/persistanceMiddleware";
import DocItemLayout from "@theme/ApiItem/Layout";
import CodeBlock from "@theme/CodeBlock";
import DocItemMetadata from "@theme/DocItem/Metadata";
import SkeletonLoader from "@theme/SkeletonLoader";
import clsx from "clsx";
import { ParameterObject, ServerObject } from "docusaurus-plugin-openapi-docs/src/openapi/types";
import type { ApiItem as ApiItemType } from "docusaurus-plugin-openapi-docs/src/types";
import type { ThemeConfig } from "docusaurus-theme-openapi-docs/src/types";
import { ungzip } from "pako";
import React from "react";
import { Provider } from "react-redux";
import { APIStore, createStoreWithState, createStoreWithoutState } from "./store";
let ApiExplorer: React.FC<ApiExplorerProps> = () => <div />;
if (ExecutionEnvironment.canUseDOM) {
// @ts-expect-error - Dynamic import
ApiExplorer = await import("@theme/ApiExplorer").then((mod) => mod.default);
}
function base64ToUint8Array(base64: string) {
const binary = atob(base64);
const len = binary.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes;
}
function decodeAPI(encodedAPI: string): ApiItemType | null {
try {
return JSON.parse(
ungzip(base64ToUint8Array(encodedAPI), {
to: "string",
}),
);
} catch (_error) {
return null;
}
}
interface APIItemSchemeProps {
content: PropDocContent;
}
const APIItemScheme: React.FC<APIItemSchemeProps> = (props) => {
const MDXComponent = props.content;
const docHtmlClassName = `docs-doc-id-${props.content.metadata.id}`;
const { frontMatter } = MDXComponent;
const { sample } = frontMatter;
return (
<DocProvider content={props.content}>
<HtmlClassNameProvider className={docHtmlClassName}>
<DocItemMetadata />
<DocItemLayout>
<div className={clsx("row", "theme-api-markdown")}>
<div className="col col--7 openapi-left-panel__container schema">
<MDXComponent />
</div>
<div className="col col--5 openapi-right-panel__container">
{sample ? (
<CodeBlock language="json" title={`${frontMatter.title}`}>
{JSON.stringify(sample, null, 2)}
</CodeBlock>
) : null}
</div>
</div>
</DocItemLayout>
</HtmlClassNameProvider>
</DocProvider>
);
};
interface APIItemAPIProps {
content: PropDocContent;
api: ApiItemType;
}
const APIItemAPI: React.FC<APIItemAPIProps> = ({ content: MDXComponent, api }) => {
const docHtmlClassName = `docs-doc-id-${MDXComponent.metadata.id}`;
const frontMatter = MDXComponent.frontMatter;
const { siteConfig } = useDocusaurusContext();
const themeConfig = siteConfig.themeConfig as ThemeConfig;
const options = themeConfig.api;
const isBrowser = useIsBrowser();
// Regex for 2XX status
const statusRegex = new RegExp("(20[0-9]|2[1-9][0-9])");
let store: APIStore;
const persistanceMiddleware = createPersistanceMiddleware(options);
// Init store for SSR
if (!isBrowser) {
store = createStoreWithoutState({}, [persistanceMiddleware]);
} else {
// Init store for CSR to hydrate components
// Create list of only 2XX response content types to create request samples from
const acceptArrayInit: string[][] = [];
for (const [code, content] of Object.entries(api.responses ?? [])) {
if (statusRegex.test(code)) {
acceptArrayInit.push(Object.keys(content.content ?? {}));
}
}
const acceptArray = acceptArrayInit.flat();
const content = api.requestBody?.content ?? {};
const contentTypeArray = Object.keys(content);
const servers = api.servers ?? [];
const params = {
path: [] as ParameterObject[],
query: [] as ParameterObject[],
header: [] as ParameterObject[],
cookie: [] as ParameterObject[],
};
api.parameters?.forEach((param: { in: "path" | "query" | "header" | "cookie" }) => {
const paramType = param.in;
const paramsArray: ParameterObject[] = params[paramType];
paramsArray.push(param as ParameterObject);
});
const auth = createAuth({
security: api.security,
securitySchemes: api.securitySchemes,
options,
});
const server = window?.sessionStorage.getItem("server");
const serverObject = (JSON.parse(server!) as ServerObject) ?? {};
store = createStoreWithState(
{
accept: {
value: acceptArray[0],
options: acceptArray,
},
contentType: {
value: contentTypeArray[0],
options: contentTypeArray,
},
server: {
value: serverObject.url ? serverObject : undefined,
options: servers,
},
response: { value: undefined },
body: { type: "empty" },
params,
auth,
},
[persistanceMiddleware],
);
}
return (
<DocProvider content={MDXComponent}>
<HtmlClassNameProvider className={docHtmlClassName}>
<DocItemMetadata />
<DocItemLayout>
<Provider store={store}>
<div className={clsx("row", "theme-api-markdown")}>
<div className="col col--7 openapi-left-panel__container">
<MDXComponent />
</div>
<div className="col col--5 openapi-right-panel__container">
<BrowserOnly fallback={<SkeletonLoader size="lg" />}>
{() => {
return (
<ApiExplorer
item={api}
infoPath={frontMatter.info_path}
/>
);
}}
</BrowserOnly>
</div>
</div>
</Provider>
</DocItemLayout>
</HtmlClassNameProvider>
</DocProvider>
);
};
interface APIItemProps {
content: PropDocContent;
}
const ApiItem: React.FC<APIItemProps> = ({ content: MDXComponent }) => {
const frontMatter = MDXComponent.frontMatter;
if (frontMatter.schema) {
return <APIItemScheme content={MDXComponent} />;
}
if (!MDXComponent.api) {
// Non-API docs
return (
<DocProvider content={MDXComponent}>
<HtmlClassNameProvider className={`docs-doc-id-${MDXComponent.metadata.id}`}>
<DocItemMetadata />
<DocItemLayout>
<div className="row">
<div className="col col--12 markdown">
<MDXComponent />
</div>
</div>
</DocItemLayout>
</HtmlClassNameProvider>
</DocProvider>
);
}
return (
<BrowserOnly fallback={<SkeletonLoader size="lg" />}>
{() => {
const api = decodeAPI(MDXComponent.api!);
if (!api) {
console.error("Failed to decode API", frontMatter);
throw new Error("Failed to decode API");
}
return <APIItemAPI content={MDXComponent} api={api} />;
}}
</BrowserOnly>
);
};
export default ApiItem;

View File

@ -1,44 +0,0 @@
import { combineReducers, configureStore } from "@reduxjs/toolkit";
import { Middleware } from "@reduxjs/toolkit";
import accept from "@theme/ApiExplorer/Accept/slice";
import auth from "@theme/ApiExplorer/Authorization/slice";
import body from "@theme/ApiExplorer/Body/slice";
import contentType from "@theme/ApiExplorer/ContentType/slice";
import params from "@theme/ApiExplorer/ParamOptions/slice";
import response from "@theme/ApiExplorer/Response/slice";
import server from "@theme/ApiExplorer/Server/slice";
const rootReducer = combineReducers({
accept,
contentType,
response,
server,
body,
params,
auth,
});
export type RootState = ReturnType<typeof rootReducer>;
export function createStoreWithState(preloadedState: RootState, middlewares: Middleware[]) {
return configureStore({
reducer: rootReducer,
preloadedState,
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(...middlewares),
});
}
export type APIStore = ReturnType<typeof createStoreWithState>;
export function createStoreWithoutState(
preloadedState: Partial<RootState>,
middlewares: Middleware[],
) {
return configureStore({
reducer: rootReducer,
preloadedState,
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(...middlewares),
});
}
export type AppDispatch = ReturnType<typeof createStoreWithState>["dispatch"];

View File

@ -1,70 +0,0 @@
import Link from "@docusaurus/Link";
import isInternalUrl from "@docusaurus/isInternalUrl";
import { isActiveSidebarItem } from "@docusaurus/plugin-content-docs/client";
import { ThemeClassNames } from "@docusaurus/theme-common";
import type { Props } from "@theme/DocSidebarItem/Link";
import IconExternalLink from "@theme/Icon/ExternalLink";
import clsx from "clsx";
import React from "react";
import "./styles.css";
const docsURL = new URL(process.env.DOCS_URL || "https://docs.goauthentik.io");
function isInternalUrlOrDocsUrl(url: string) {
if (isInternalUrl(url)) return true;
const inputURL = new URL(url);
return inputURL.origin === docsURL.origin;
}
const DocSidebarItemLink: React.FC<Props> = ({
item,
onItemClick,
activePath,
level,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
index,
...props
}) => {
const { href, label, className, autoAddBaseUrl } = item;
const isActive = isActiveSidebarItem(item, activePath);
const internalLink = isInternalUrlOrDocsUrl(href);
return (
<li
className={clsx(
ThemeClassNames.docs.docSidebarItemLink,
ThemeClassNames.docs.docSidebarItemLinkLevel(level),
"menu__list-item",
className,
)}
key={label}
>
<Link
className={clsx("menu__link", {
"menu__link--external": !internalLink,
"menu__link--active": isActive,
})}
autoAddBaseUrl={autoAddBaseUrl}
aria-current={isActive ? "page" : undefined}
to={href}
{...(internalLink && {
onClick: onItemClick ? () => onItemClick(item) : undefined,
})}
{...props}
>
{item.className?.includes("api-method") ? (
<div className="badge-container">
<span role="img" className="badge method" />
</div>
) : null}
{label}
{!internalLink && <IconExternalLink />}
</Link>
</li>
);
};
export default DocSidebarItemLink;

View File

@ -1,127 +0,0 @@
.theme-layout-main {
--doc-sidebar-width: 400px;
}
.navbar-sidebar__upwards {
.menu__link {
font-size: var(--ifm-h6-font-size);
font-weight: var(--ifm-font-weight-bold);
color: var(--ifm-color-info-light);
padding-block: calc(var(--ifm-spacing-vertical) / 1.5);
}
}
.theme-doc-sidebar-item-category.api-reference {
> .menu__list-item-collapsible {
font-weight: 900;
font-size: var(--ifm-h3-font-size);
}
.menu__list {
padding-left: 0;
}
.theme-doc-sidebar-item-category-level-2 .menu__list-item-collapsible {
font-size: var(--ifm-h4-font-size);
font-weight: bold;
text-transform: capitalize;
}
}
.menu__link.menu__link--external {
align-items: center;
}
.menu__list-item.api-method {
.badge-container {
flex: 0 0 auto;
display: flex;
width: 100%;
justify-content: end;
}
> .menu__link {
--menu-border-width: 2px;
color: var(--menu-item-contrast-foreground, red);
background-color: var(--menu-item-background-color, transparent);
flex-flow: column;
font-family: var(--ifm-font-family-monospace);
font-weight: 600;
gap: 0.25em;
padding-inline-end: 0.25em;
word-break: break-all;
align-items: start;
border-radius: 0;
margin-inline-end: calc(var(--ifm-menu-link-padding-horizontal) / 2);
font-size: var(--ifm-h6-font-size);
letter-spacing: 0.015em;
text-rendering: optimizelegibility;
position: relative;
box-shadow: var(--ifm-global-shadow-lw);
&::before {
position: absolute;
left: 0;
top: 0;
width: var(--menu-border-width);
height: 100%;
display: block;
z-index: 1;
background-color: var(--ifm-badge-color, var(--ifm-color-primary));
content: "";
transition: width 0.2s var(--ifm-transition-timing-default);
}
&:hover,
&.menu__link--active {
--menu-border-width: 6px;
}
}
&.get {
--method-label: "GET";
--menu-item-contrast-foreground: var(--ifm-color-content);
--menu-item-background-color: var(--ifm-card-background-color);
--ifm-badge-color: var(--ifm-color-primary-light);
}
&.post {
--method-label: "POST";
--menu-item-contrast-foreground: var(--ifm-color-success-contrast-foreground);
--menu-item-background-color: var(--ifm-color-success-contrast-background);
--ifm-badge-color: var(--ifm-color-success-lightest);
}
&.put {
--method-label: "PUT";
--menu-item-contrast-foreground: var(--ifm-color-info-contrast-foreground);
--menu-item-background-color: var(--ifm-color-info-contrast-background);
--ifm-badge-color: var(--ifm-color-info-lightest);
}
&.patch {
--method-label: "PATCH";
--menu-item-contrast-foreground: var(--ifm-color-warning-contrast-foreground);
--menu-item-background-color: var(--ifm-color-warning-contrast-background);
--ifm-badge-color: var(--ifm-color-warning-lightest);
}
&.delete {
--method-label: "DELETE";
--menu-item-contrast-foreground: var(--ifm-color-danger-contrast-foreground);
--menu-item-background-color: var(--ifm-color-danger-contrast-background);
--ifm-badge-color: var(--ifm-color-danger-lightest);
}
}
.badge.method {
position: relative;
flex: 0 0 auto;
user-select: none;
&::before {
content: var(--method-label, "METHOD");
display: block;
}
}

View File

@ -1,8 +0,0 @@
{
"extends": "../tsconfig.base.json",
"references": [
{
"path": "../docusaurus-theme"
}
]
}

View File

@ -1,37 +0,0 @@
/// <reference types="docusaurus-theme-openapi-docs" />
/// <reference types="docusaurus-plugin-openapi-docs" />
declare module "@docusaurus/plugin-content-docs/src/sidebars/types" {
export * from "@docusaurus/plugin-content-docs/src/sidebars/types.ts";
}
declare module "@theme/RequestSchema";
declare module "@theme/ParamsDetails";
declare module "@theme/StatusCodes";
declare module "@theme/OperationTabs";
declare module "@theme/SkeletonLoader" {
import { FC } from "react";
const SkeletonLoader: FC<{ size: "sm" | "md" | "lg" }>;
export default SkeletonLoader;
}
declare module "@theme/APIExplorer" {
import { FC } from "react";
export interface ApiExplorerProps {
item: unknown;
infoPath: unknown;
}
const ApiExplorer: FC<ApiExplorerProps>;
export default ApiExplorer;
}
declare module "@theme/ApiExplorer/persistanceMiddleware" {
import { Middleware } from "@reduxjs/toolkit";
import type { ThemeConfig } from "docusaurus-theme-openapi-docs/src/types";
export const createPersistanceMiddleware: (options: ThemeConfig["api"]) => Middleware;
}

View File

@ -1,34 +0,0 @@
/**
* @file Supplemental type definitions for Docusaurus.
*
* @remarks
*
* Docusaurus uses an unconventional module resolution strategy, which can lead to
* issues when using TypeScript.
*
* The types in this file are intended to expose less visible types to TypeScript's
* project references, allowing for better type checking and autocompletion.
*/
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
/// <reference types="@docusaurus/plugin-content-docs" />
/// <reference types="@docusaurus/theme-classic" />
import type { PropDocContent as BasePropDocContent } from "@docusaurus/plugin-content-docs";
import type { DocContextValue as BaseDocContextValue } from "@docusaurus/plugin-content-docs/client";
declare global {
export interface APIDocFrontMatter {
readonly info_path?: string;
readonly api?: string;
readonly schema?: boolean;
readonly sample?: unknown;
}
export interface PropDocContent extends BasePropDocContent {
readonly api?: string;
frontMatter: APIDocFrontMatter & BasePropDocContent["frontMatter"];
}
export interface DocContextValue extends BaseDocContextValue {
frontMatter: APIDocFrontMatter & BasePropDocContent["frontMatter"];
}
}

View File

@ -1,97 +0,0 @@
import {
createVersionURL,
isPrerelease,
parseHostnameSemVer,
} from "#components/VersionPicker/utils.ts";
import clsx from "clsx";
import React, { memo } from "react";
import "./styles.css";
export interface VersionDropdownProps {
/**
* The hostname of the client.
*/
hostname: string | null;
/**
* The origin of the prerelease documentation.
*
* @format url
*/
prereleaseOrigin: string;
/**
* The available versions of the documentation.
*
* @format semver
*/
releases: string[];
}
/**
* A dropdown that shows the available versions of the documentation.
*/
export const VersionDropdown = memo<VersionDropdownProps>(
({ hostname, prereleaseOrigin, releases }) => {
const prerelease = isPrerelease(hostname);
const parsedSemVer = !prerelease ? parseHostnameSemVer(hostname) : null;
const currentLabel = parsedSemVer || "Pre-Release";
const endIndex = parsedSemVer ? releases.indexOf(parsedSemVer) : -1;
const visibleReleases = releases.slice(0, endIndex === -1 ? 3 : endIndex + 3);
return (
<li className="navbar__item dropdown dropdown--hoverable dropdown--right ak-version-selector">
<div
aria-haspopup="true"
aria-expanded="false"
role="button"
className="navbar__link menu__link"
>
Version: {currentLabel}
</div>
<ul className="dropdown__menu menu__list-item--collapsed">
{!prerelease ? (
<li>
<a
href={prereleaseOrigin}
target="_blank"
rel="noopener noreferrer"
className="dropdown__link menu__link"
>
Pre-Release
</a>
</li>
) : null}
{visibleReleases.map((semVer, idx) => {
const label = semVer;
// TODO: Flesh this out after we settle on versioning strategy.
// if (idx === 0) {
// label += " (Current Release)";
// }
return (
<li key={idx}>
<a
href={createVersionURL(semVer)}
target="_blank"
rel="noopener noreferrer"
className={clsx("dropdown__link menu__link", {
"menu__link--active": semVer === currentLabel,
})}
>
{label}
</a>
</li>
);
})}
</ul>
</li>
);
},
);

View File

@ -1,76 +0,0 @@
import { VersionDropdown } from "#components/VersionPicker/VersionDropdown.tsx";
import { LocalhostAliases, ProductionURL, useHostname } from "#components/VersionPicker/utils.ts";
import { AKReleasesPluginData } from "@goauthentik/docusaurus-theme/releases/plugin";
import useIsBrowser from "@docusaurus/useIsBrowser";
import React, { useEffect, useMemo, useState } from "react";
export interface VersionPickerLoaderProps {
pluginData: AKReleasesPluginData;
}
/**
* A data-fetching component that loads available versions of the documentation.
*
* @see {@linkcode VersionPicker} for the component.
* @see {@linkcode AKReleasesPluginData} for the plugin data.
* @client
*/
export const VersionPickerLoader: React.FC<VersionPickerLoaderProps> = ({ pluginData }) => {
const [releases, setReleases] = useState(pluginData.releases);
const browser = useIsBrowser();
const hostname = useHostname();
const prereleaseOrigin = useMemo(() => {
if (browser && LocalhostAliases.has(window.location.hostname)) {
return window.location.origin;
}
return ProductionURL.href;
}, [browser]);
useEffect(() => {
if (!browser || !prereleaseOrigin) return;
const controller = new AbortController();
const updateURL = new URL(pluginData.publicPath, prereleaseOrigin);
fetch(updateURL, {
signal: controller.signal,
})
.then((response) => {
if (!response.ok) {
throw new Error(`Failed to fetch new releases: ${response.status}`);
}
return response.json();
})
.then((data: unknown) => {
// We're extra cautious here to be ready if the API shape ever changes.
if (!data) throw new Error("Failed to parse releases");
if (!Array.isArray(data)) throw new Error("Releases must be an array");
if (!data.every((item) => typeof item === "string"))
throw new Error("Releases must be an array of strings");
setReleases(data);
})
.catch((error) => {
console.warn(`Failed to fetch new releases: ${error}`);
});
// eslint-disable-next-line consistent-return
return () => controller.abort("unmount");
}, [browser, pluginData.publicPath, prereleaseOrigin]);
return (
<VersionDropdown
hostname={hostname}
prereleaseOrigin={prereleaseOrigin}
releases={releases}
/>
);
};

View File

@ -1,32 +0,0 @@
import { VersionDropdown } from "#components/VersionPicker/VersionDropdown.tsx";
import { useHostname, usePrereleaseOrigin } from "#components/VersionPicker/utils.ts";
import { AKReleasesPluginData } from "@goauthentik/docusaurus-theme/releases/plugin";
import { usePluginData } from "@docusaurus/useGlobalData";
/**
* A component that shows the available versions of the documentation.
*
* @see {@linkcode VersionPickerLoader} for the data-fetching component.
*/
export const VersionPicker: React.FC = () => {
const hostname = useHostname();
const prereleaseOrigin = usePrereleaseOrigin();
const pluginData = usePluginData("ak-releases-plugin", undefined) as
| AKReleasesPluginData
| undefined;
if (!pluginData?.releases.length) return null;
// return <VersionPickerLoader pluginData={pluginData} />;
return (
<VersionDropdown
hostname={hostname}
prereleaseOrigin={prereleaseOrigin}
releases={pluginData.releases}
/>
);
};

View File

@ -1,33 +0,0 @@
.theme-doc-sidebar-menu {
--ak-version-selector-padding: calc(var(--ifm-spacing-vertical) / 2);
.dropdown.ak-version-selector {
width: calc(100% - (var(--ifm-spacing-horizontal) / 2));
border-block-end: var(--ifm-hr-height) solid var(--ifm-color-emphasis-200);
padding-block-start: calc(var(--ak-version-selector-padding) / 2);
padding-block-end: var(--ak-version-selector-padding);
margin-block-end: var(--ak-version-selector-padding);
&:has(+ .navbar-sidebar__upwards) {
margin-block-end: 0;
}
.navbar__link.menu__link {
display: flex;
width: 100%;
justify-content: space-between;
font-weight: var(--ifm-font-weight-semibold);
&::after {
color: var(--ifm-color-emphasis-400);
filter: var(--ifm-menu-link-sublist-icon-filter);
}
}
.dropdown__menu {
background: var(--ifm-dropdown-background-color);
box-shadow: var(--ifm-global-shadow-lw);
border: 1px solid var(--ifm-color-emphasis-200);
}
}
}

View File

@ -1,81 +0,0 @@
import useIsBrowser from "@docusaurus/useIsBrowser";
import { useMemo } from "react";
import { coerce } from "semver";
export const ProductionURL = new URL("https://docs.goauthentik.io");
export const LocalhostAliases: ReadonlySet<string> = new Set(["localhost", "127.0.0.1"]);
/**
* Given a semver, create the URL for the version.
*/
export function createVersionURL(semver: string): string {
const subdomain = `version-${semver.replace(".", "-")}`;
return `https://${subdomain}.goauthentik.io`;
}
/**
* Predicate to determine if a hostname appears to be a prerelease origin.
*/
export function isPrerelease(hostname: string | null): boolean {
if (!hostname) return false;
if (hostname === ProductionURL.hostname) return true;
if (hostname.endsWith(".netlify.app")) return true;
if (LocalhostAliases.has(hostname)) return true;
return false;
}
/**
* Given a hostname, parse the semver from the subdomain.
*/
export function parseHostnameSemVer(hostname: string | null): string | null {
if (!hostname) return null;
const [, possibleSemVer] = hostname.match(/version-(.+)\.goauthentik\.io/) || [];
if (!possibleSemVer) return null;
const formattedSemVer = possibleSemVer.replace("-", ".");
if (!coerce(formattedSemVer)) return null;
return formattedSemVer;
}
export function useHostname() {
const browser = useIsBrowser();
const hostname = useMemo(() => {
if (!browser) return null;
const searchParams = new URLSearchParams(window.location.search);
// Query parameter used for debugging.
// Note that this doesn't synchronize with Docusaurus's router state.
const subdomain = searchParams.get("version");
if (subdomain) return subdomain;
return window.location.hostname;
}, [browser]);
return hostname;
}
export function usePrereleaseOrigin() {
const browser = useIsBrowser();
const prereleaseOrigin = useMemo(() => {
if (browser && LocalhostAliases.has(window.location.hostname)) {
return window.location.origin;
}
return ProductionURL.href;
}, [browser]);
return prereleaseOrigin;
}

View File

@ -1,85 +0,0 @@
/**
* @file Docusaurus config.
*
* @import { Config } from "@docusaurus/types";
* @import { UserThemeConfig, UserThemeConfigExtra } from "@goauthentik/docusaurus-config";
* @import { Options as DocsPluginOptions } from "@docusaurus/plugin-content-docs";
* @import { BuildUrlValues } from "remark-github";
*/
import {
remarkEnterpriseDirective,
remarkPreviewDirective,
remarkSupportDirective,
remarkVersionDirective,
} from "#remark";
import remarkNPM2Yarn from "@docusaurus/remark-plugin-npm2yarn";
import remarkDirective from "remark-directive";
import remarkGithub, { defaultBuildUrl } from "remark-github";
//#region Common configuration
/**
* @satisfies {DocsPluginOptions}
*/
export const CommonDocsPluginOptions = {
id: "docs",
routeBasePath: "/",
path: "docs",
sidebarPath: "./docs/sidebar.mjs",
showLastUpdateTime: false,
editUrl: "https://github.com/goauthentik/authentik/edit/main/docs/",
//#region Docs Plugins
beforeDefaultRemarkPlugins: [
remarkDirective,
remarkVersionDirective,
remarkEnterpriseDirective,
remarkPreviewDirective,
remarkSupportDirective,
],
remarkPlugins: [
[remarkNPM2Yarn, { sync: true }],
[
remarkGithub,
{
repository: "goauthentik/authentik",
/**
* @param {BuildUrlValues} values
*/
buildUrl: (values) => {
// Only replace issues and PR links
return values.type === "issue" || values.type === "mention"
? defaultBuildUrl(values)
: false;
},
},
],
],
};
/**
* Documentation site configuration for Docusaurus.
* @satisfies {Partial<Config>}
*/
export const CommonConfig = {
themes: ["@docusaurus/theme-mermaid"],
themeConfig: /** @type {UserThemeConfig & UserThemeConfigExtra} */ ({
algolia: {
appId: "36ROD0O0FV",
apiKey: "727db511300ca9aec5425645bbbddfb5",
indexName: "goauthentik",
},
}),
plugins: [
[
"@docusaurus/plugin-google-gtag",
{
trackingID: ["G-9MVR9WZFZH"],
anonymizeIP: true,
},
],
],
};

View File

@ -1,3 +0,0 @@
import { createESLintPackageConfig } from "@goauthentik/eslint-config";
export default createESLintPackageConfig();

View File

@ -1,21 +0,0 @@
/**
* @file Docusaurus theme plugin.
* @import { Plugin } from "@docusaurus/types";
*/
/**
* @returns {Plugin<void>}
*/
export default function docusaurusThemeAuthentik() {
return {
name: "docusaurus-theme-authentik",
getThemePath() {
return "./theme";
},
getTypeScriptThemePath() {
return "./theme";
},
};
}

View File

@ -1,23 +0,0 @@
{
"name": "@goauthentik/docusaurus-theme",
"version": "0.0.0",
"license": "MIT",
"private": true,
"type": "module",
"exports": {
"./package.json": "./package.json",
".": "./index.js",
"./config": "./config.js",
"./remark": "./remark/index.mjs",
"./components/*": "./components/*",
"./releases/plugin": "./releases/plugin.mjs",
"./releases/utils": "./releases/utils.mjs"
},
"imports": {
"#remark": "./remark/index.mjs",
"#remark/*": "./remark/*",
"#components/*": "./components/*",
"#hooks/*": "./hooks/*",
"#theme/*": "./theme/*"
}
}

View File

@ -1,65 +0,0 @@
/* eslint-disable no-console */
/**
* @file Docusaurus releases plugin.
*
* @import { LoadContext, Plugin } from "@docusaurus/types"
*/
import * as fs from "node:fs/promises";
import * as path from "node:path";
import { collectReleaseFiles } from "./utils.mjs";
const PLUGIN_NAME = "ak-releases-plugin";
const RELEASES_FILENAME = "releases.gen.json";
/**
* @typedef {object} ReleasesPluginOptions
* @property {string} docsDirectory The path to the documentation directory.
*/
/**
* @typedef {object} AKReleasesPluginData
* @property {string} publicPath The URL to the plugin's public directory.
* @property {string[]} releases The available versions of the documentation.
*/
/**
* @param {LoadContext} loadContext
* @param {ReleasesPluginOptions} options
* @returns {Promise<Plugin<AKReleasesPluginData>>}
*/
async function akReleasesPlugin(loadContext, { docsDirectory }) {
return {
name: PLUGIN_NAME,
async loadContent() {
console.log(`🚀 ${PLUGIN_NAME} loaded`);
const releases = collectReleaseFiles(docsDirectory).map((release) => release.name);
const outputPath = path.join(loadContext.siteDir, "static", RELEASES_FILENAME);
await fs.mkdir(path.dirname(outputPath), { recursive: true });
await fs.writeFile(outputPath, JSON.stringify(releases, null, 2), "utf-8");
console.log(`${RELEASES_FILENAME} generated`);
/**
* @type {AKReleasesPluginData}
*/
const content = {
releases,
publicPath: path.join("/", RELEASES_FILENAME),
};
return content;
},
contentLoaded({ content, actions }) {
const { setGlobalData } = actions;
setGlobalData(content);
},
};
}
export default akReleasesPlugin;

View File

@ -1,69 +0,0 @@
/**
* @file Docusaurus release utils.
*
* @import { SidebarItemConfig } from "@docusaurus/plugin-content-docs/src/sidebars/types.js"
*/
import FastGlob from "fast-glob";
import * as path from "node:path";
import { coerce } from "semver";
/**
*
* @param {string} releasesParentDirectory
* @returns {FastGlob.Entry[]}
*/
export function collectReleaseFiles(releasesParentDirectory) {
const releaseFiles = FastGlob.sync("releases/**/v*.{md,mdx}", {
cwd: releasesParentDirectory,
onlyFiles: true,
objectMode: true,
})
.map((fileEntry) => {
return {
...fileEntry,
path: fileEntry.path.replace(/\.mdx?$/, ""),
name: fileEntry.name.replace(/^v/, "").replace(/\.mdx?$/, ""),
};
})
.sort((a, b) => {
const aSemVer = coerce(a.name);
const bSemVer = coerce(b.name);
if (aSemVer && bSemVer) {
return bSemVer.compare(aSemVer);
}
return b.name.localeCompare(a.name);
});
return releaseFiles;
}
export const SUPPORTED_RELEASE_COUNT = 3;
/**
*
* @param {FastGlob.Entry[]} releaseFiles
*/
export function createReleaseSidebarEntries(releaseFiles) {
/**
* @type {SidebarItemConfig[]}
*/
let sidebarEntries = releaseFiles.map((fileEntry) => {
return path.join(fileEntry.path);
});
if (releaseFiles.length > SUPPORTED_RELEASE_COUNT) {
// Then we add the rest of the releases as a category.
sidebarEntries = [
...sidebarEntries.slice(0, SUPPORTED_RELEASE_COUNT),
{
type: "category",
label: "Previous versions",
items: sidebarEntries.slice(SUPPORTED_RELEASE_COUNT),
},
];
}
return sidebarEntries;
}

View File

@ -1,5 +0,0 @@
export * from "./enterprise-directive.mjs";
export * from "./link-rewrite-directive.mjs";
export * from "./preview-directive.mjs";
export * from "./support-directive.mjs";
export * from "./version-directive.mjs";

View File

@ -1,35 +0,0 @@
/**
* @import { Root } from "mdast";
*/
import { SKIP, visit } from "unist-util-visit";
/**
* @typedef {[pattern: string | RegExp, replacement: string]} Rewrite
*/
/**
* Remark plugin to transform relative links to docs to absolute URLs
* @param {Iterable<[string, string]>} rewrites Map of urls to rewrite where the key is the prefix to check for and the value is the domain to add
*/
export function remarkLinkRewrite(rewrites) {
const map = new Map(rewrites);
return () => {
/**
* @param {Root} tree The MDAST tree to transform.
*/
return (tree) => {
visit(tree, "link", (node) => {
for (const [pattern, replacement] of map) {
if (!node.url.startsWith(pattern)) continue;
node.url = node.url.replace(pattern, replacement);
}
return SKIP;
});
};
};
}
export default remarkLinkRewrite;

View File

@ -1,26 +0,0 @@
/// <reference types="@docusaurus/plugin-content-docs" />
import { VersionPicker } from "#components/VersionPicker/index.tsx";
import {
DocSidebarItemsExpandedStateProvider,
useVisibleSidebarItems,
} from "@docusaurus/plugin-content-docs/client";
import DocSidebarItem from "@theme/DocSidebarItem";
import type { Props as DocSidebarItemsProps } from "@theme/DocSidebarItems";
import { memo } from "react";
const DocSidebarItems = ({ items, ...props }: DocSidebarItemsProps): JSX.Element => {
const visibleItems = useVisibleSidebarItems(items, props.activePath);
const includeVersionPicker = props.level === 1 && !props.activePath.startsWith("/integrations");
return (
<DocSidebarItemsExpandedStateProvider>
{includeVersionPicker ? <VersionPicker /> : null}
{visibleItems.map((item, index) => (
<DocSidebarItem key={index} item={item} index={index} {...props} />
))}
</DocSidebarItemsExpandedStateProvider>
);
};
export default memo(DocSidebarItems);

View File

@ -1,3 +0,0 @@
{
"extends": "../tsconfig.base.json"
}

View File

@ -1,44 +0,0 @@
/**
* @file Supplemental type definitions for Docusaurus.
*
* @remarks
*
* Docusaurus uses an unconventional module resolution strategy, which can lead to
* issues when using TypeScript.
*
* The types in this file are intended to expose less visible types to TypeScript's
* project references, allowing for better type checking and autocompletion.
*/
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
/// <reference types="@docusaurus/plugin-content-docs" />
/// <reference types="@docusaurus/theme-classic" />
import type { PropDocContent as BasePropDocContent } from "@docusaurus/plugin-content-docs";
import type { DocContextValue as BaseDocContextValue } from "@docusaurus/plugin-content-docs/client";
declare global {
/**
* @monkeypatch
*/
export interface DocFrontMatter {
support_level?: string;
authentik_version?: string;
authentik_preview: boolean;
authentik_enterprise: boolean;
}
export interface APIDocFrontMatter {
readonly info_path?: string;
readonly api?: string;
readonly schema?: boolean;
readonly sample?: unknown;
}
export interface PropDocContent extends BasePropDocContent {
readonly api?: string;
frontMatter: APIDocFrontMatter & BasePropDocContent["frontMatter"];
}
export interface DocContextValue extends BaseDocContextValue {
frontMatter: DocFrontMatter & APIDocFrontMatter & BasePropDocContent["frontMatter"];
}
}

View File

@ -1,11 +0,0 @@
import { DefaultIgnorePatterns, createESLintPackageConfig } from "@goauthentik/eslint-config";
export default createESLintPackageConfig({
ignorePatterns: [
// ---
...DefaultIgnorePatterns,
"**/.docusaurus/",
"**/build",
"**/reference",
],
});

View File

@ -1,7 +0,0 @@
.theme-doc-sidebar-item-category-level-1 .menu__list-item-collapsible {
border-top: 0.5px solid;
border-top-color: var(--ifm-category-color, var(--ifm-menu-color-background-active));
border-radius: 0;
font-weight: 600;
padding-block: 0.25em;
}

View File

@ -1 +0,0 @@
module.exports = import("./docusaurus.config.esm.mjs").then(($) => $.default);

View File

@ -1,87 +0,0 @@
/**
* @file Docusaurus Integrations config.
*
* @import { Config } from "@docusaurus/types";
* @import { UserThemeConfig, UserThemeConfigExtra } from "@goauthentik/docusaurus-config";
* @import { Options as DocsPluginOptions } from "@docusaurus/plugin-content-docs";
*/
import { createDocusaurusConfig } from "@goauthentik/docusaurus-config";
import { CommonConfig, CommonDocsPluginOptions } from "@goauthentik/docusaurus-theme/config";
import { remarkLinkRewrite } from "@goauthentik/docusaurus-theme/remark";
import { GlobExcludeDefault } from "@docusaurus/utils";
import { deepmerge } from "deepmerge-ts";
import { createRequire } from "node:module";
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";
const require = createRequire(import.meta.url);
const __dirname = fileURLToPath(new URL(".", import.meta.url));
//#region Configuration
/**
* Documentation site configuration for Docusaurus.
* @satisfies {Partial<Config>}
*/
const config = {
staticDirectories: [
// ---
resolve(__dirname, "..", "static"),
"static",
],
themes: ["@goauthentik/docusaurus-theme"],
themeConfig: /** @type {UserThemeConfig & UserThemeConfigExtra} */ ({
navbarReplacements: {
INTEGRATIONS_URL: "/",
},
algolia: {
externalUrlRegex: /^(?:https?:\/\/)(integrations|api).?(goauthentik.io)/.source,
},
}),
plugins: [
[
"@docusaurus/theme-classic",
{
customCss: [
"./custom.css",
require.resolve("@goauthentik/docusaurus-config/css/index.css"),
],
},
],
//#region Documentation
[
"@docusaurus/plugin-content-docs",
deepmerge(
CommonDocsPluginOptions,
/** @type {DocsPluginOptions} */ ({
id: "docs",
routeBasePath: "/",
path: ".",
exclude: [...GlobExcludeDefault],
include: ["**/*.mdx", "**/*.md"],
sidebarPath: "./sidebar.mjs",
showLastUpdateTime: false,
editUrl:
"https://github.com/goauthentik/authentik/edit/main/docs/topics/integrations/",
//#region Docs Plugins
beforeDefaultRemarkPlugins: [
remarkLinkRewrite([
// ---
["/api", "https://api.goauthentik.io"],
["/docs", "https://docs.goauthentik.io"],
]),
],
}),
),
],
],
};
export default /** @type {Config} */ (deepmerge(CommonConfig, createDocusaurusConfig(config)));

View File

@ -1,10 +0,0 @@
import { DefaultIgnorePatterns, createESLintPackageConfig } from "@goauthentik/eslint-config";
export default createESLintPackageConfig({
ignorePatterns: [
// ---
...DefaultIgnorePatterns,
".docusaurus/",
"./build",
],
});

View File

@ -1,30 +0,0 @@
[[plugins]]
package = "netlify-plugin-cache"
[plugins.inputs]
paths = [".docusaurus", ".cache", 'node_modules/.cache']
[[plugins]]
package = "netlify-plugin-debug-cache"
[build]
base = "docs"
package = "integrations"
command = "npm run build -w integrations"
publish = "integrations/build"
[dev]
command = "npm start"
targetPort = 3000
publish = "integrations/build"
[context.production.environment]
NODE_ENV = "production"
[context.dev.environment]
NODE_ENV = "development"
[[headers]]
for = "/*"
[headers.values]
X-Frame-Options = "DENY"

View File

@ -1,22 +0,0 @@
{
"name": "@goauthentik/integration-docs",
"version": "0.0.0",
"license": "MIT",
"private": true,
"scripts": {
"build": "run-s build:types build:docusaurus",
"build:docusaurus": "docusaurus build",
"build:types": "tsc -b .",
"deploy": "docusaurus deploy",
"docusaurus": "docusaurus",
"serve": "docusaurus serve",
"start": "docusaurus start",
"test": "node --test"
},
"dependencies": {
"@goauthentik/docusaurus-theme": "*"
},
"engines": {
"node": ">=24"
}
}

View File

@ -1,49 +0,0 @@
/**
* @file Sidebar configuration for the authentik integrations.
*
* @import { SidebarItemConfig } from "@docusaurus/plugin-content-docs/src/sidebars/types.js"
*/
/**
* @type {ReadonlyArray<readonly [string, string]>}
*/
const categories = [
["chat-communication-collaboration", "Chat, Communication & Collaboration"],
["device-management", "Device Management"],
["cloud-providers", "Cloud Providers"],
["dashboards", "Dashboards"],
["development", "Development"],
["documentation", "Documentation"],
["hypervisors-orchestrators", "Hypervisors / Orchestrators"],
["infrastructure", "Infrastructure"],
["networking", "Networking"],
["media", "Media"],
["miscellaneous", "Miscellaneous"],
["monitoring", "Monitoring"],
["platforms", "Platforms"],
["security", "Security"],
];
export default /** @type {SidebarItemConfig} */
({
integrations: [
{
type: "doc",
id: "index",
},
{
type: "doc",
id: "applications",
},
...categories.map(([dirName, label]) => ({
type: "category",
label,
items: [
{
type: "autogenerated",
dirName,
},
],
})),
],
});

View File

@ -1,8 +0,0 @@
{
"extends": "../tsconfig.base.json",
"references": [
{
"path": "../docusaurus-theme"
}
]
}

View File

@ -1,13 +0,0 @@
/**
* @file Supplemental type definitions for Docusaurus.
*
* @remarks
*
* Docusaurus uses an unconventional module resolution strategy, which can lead to
* issues when using TypeScript.
*
* The types in this file are intended to expose less visible types to TypeScript's
* project references, allowing for better type checking and autocompletion.
*/
/// <reference types="@docusaurus/plugin-content-docs" />
/// <reference types="@docusaurus/theme-classic" />

View File

@ -1,101 +0,0 @@
{
"name": "@goauthentik/docs",
"version": "0.0.0",
"license": "MIT",
"private": true,
"scripts": {
"build": "run-s build:types build:docusaurus",
"build:docusaurus": "npm run build -w topics",
"build:types": "tsc -b",
"docusaurus": "docusaurus",
"lint": "eslint --fix .",
"lint-check": "eslint --max-warnings 0 .",
"prettier": "prettier --write .",
"prettier-check": "prettier --check .",
"start": "npm start -w topics"
},
"dependencies": {
"@docusaurus/core": "^3.8.1",
"@docusaurus/faster": "^3.8.1",
"@docusaurus/module-type-aliases": "^3.8.1",
"@docusaurus/plugin-client-redirects": "^3.8.1",
"@docusaurus/plugin-content-docs": "^3.8.1",
"@docusaurus/preset-classic": "^3.8.1",
"@docusaurus/remark-plugin-npm2yarn": "^3.8.1",
"@docusaurus/theme-common": "^3.8.1",
"@docusaurus/theme-mermaid": "^3.8.1",
"@docusaurus/tsconfig": "^3.8.1",
"@docusaurus/types": "^3.8.1",
"@eslint/js": "^9.29.0",
"@goauthentik/docusaurus-config": "^2.1.1",
"@goauthentik/docusaurus-theme": "*",
"@goauthentik/eslint-config": "^1.0.5",
"@goauthentik/prettier-config": "^2.0.1",
"@goauthentik/tsconfig": "^1.0.4",
"@mdx-js/react": "^3.1.0",
"@reduxjs/toolkit": "^1.7.1",
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
"@types/lodash": "^4.17.18",
"@types/node": "^24.0.3",
"@types/pako": "^2.0.3",
"@types/pidusage": "^2.0.5",
"@types/postman-collection": "^3.5.11",
"@types/react": "^18.3.23",
"@types/react-dom": "^18.3.7",
"@types/semver": "^7.7.0",
"@typescript-eslint/eslint-plugin": "^8.34.1",
"@typescript-eslint/parser": "^8.34.1",
"clsx": "^2.1.1",
"cross-env": "^7.0.3",
"docusaurus-plugin-openapi-docs": "^4.4.0",
"docusaurus-theme-openapi-docs": "^4.4.0",
"eslint": "^9.29.0",
"fast-glob": "^3.3.3",
"netlify-cli": "^22.2.1",
"netlify-plugin-cache": "^1.0.3",
"npm-run-all": "^4.1.5",
"pako": "^2.1.0",
"pidtree": "^0.6.0",
"pidusage": "^4.0.1",
"postcss": "^8.5.6",
"prettier": "^3.5.3",
"prettier-plugin-packagejson": "^2.5.15",
"prism-react-renderer": "^2.4.1",
"react": "^18.3.1",
"react-before-after-slider-component": "^1.1.8",
"react-dom": "^18.3.1",
"react-redux": "^7.2.0",
"remark-directive": "^4.0.0",
"remark-github": "^12.0.0",
"semver": "^7.7.2",
"typescript": "^5.8.3",
"typescript-eslint": "^8.35.0"
},
"optionalDependencies": {
"@rspack/binding-darwin-arm64": "1.3.15",
"@rspack/binding-linux-arm64-gnu": "1.3.15",
"@rspack/binding-linux-x64-gnu": "1.3.15",
"@swc/core-darwin-arm64": "1.12.7",
"@swc/core-linux-arm64-gnu": "1.12.7",
"@swc/core-linux-x64-gnu": "1.12.7",
"@swc/html-darwin-arm64": "1.12.7",
"@swc/html-linux-arm64-gnu": "1.12.7",
"@swc/html-linux-x64-gnu": "1.12.7",
"lightningcss-darwin-arm64": "1.30.1",
"lightningcss-linux-arm64-gnu": "1.30.1",
"lightningcss-linux-x64-gnu": "1.30.1"
},
"engines": {
"node": ">=24"
},
"workspaces": [
"api",
"docusaurus-theme",
"integrations",
"topics"
],
"prettier": "@goauthentik/prettier-config",
"overrides": {
"@rspack/core": "1.4.0-rc.0"
}
}

View File

@ -1 +0,0 @@
module.exports = import("./docusaurus.config.esm.mjs").then(($) => $.default);

View File

@ -1,92 +0,0 @@
/**
* @file Docusaurus Documentation config.
*
* @import { Config } from "@docusaurus/types";
* @import { UserThemeConfig, UserThemeConfigExtra } from "@goauthentik/docusaurus-config";
* @import { Options as DocsPluginOptions } from "@docusaurus/plugin-content-docs";
* @import { ReleasesPluginOptions } from "@goauthentik/docusaurus-theme/releases/plugin"
*/
import { createDocusaurusConfig } from "@goauthentik/docusaurus-config";
import { CommonConfig, CommonDocsPluginOptions } from "@goauthentik/docusaurus-theme/config";
import { remarkLinkRewrite } from "@goauthentik/docusaurus-theme/remark";
import { GlobExcludeDefault } from "@docusaurus/utils";
import { deepmerge } from "deepmerge-ts";
import { createRequire } from "node:module";
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = fileURLToPath(new URL(".", import.meta.url));
const require = createRequire(import.meta.url);
/**
* Documentation site configuration for Docusaurus.
* @satisfies {Partial<Config>}
*/
const config = {
staticDirectories: [
// ---
resolve(__dirname, "..", "static"),
"static",
],
themes: ["@goauthentik/docusaurus-theme"],
themeConfig: /** @type {UserThemeConfig & UserThemeConfigExtra} */ ({
navbarReplacements: {
DOCS_URL: "/",
},
algolia: {
externalUrlRegex: /^(?:https?:\/\/)(integrations|api).?(goauthentik.io)/.source,
},
}),
plugins: [
[
"@docusaurus/theme-classic",
{
customCss: require.resolve("@goauthentik/docusaurus-config/css/index.css"),
},
],
[
"@goauthentik/docusaurus-theme/releases/plugin",
/** @type {ReleasesPluginOptions} */ ({
docsDirectory: __dirname,
}),
],
//#region Documentation
[
"@docusaurus/plugin-content-docs",
deepmerge(
CommonDocsPluginOptions,
/** @type {DocsPluginOptions} */ ({
id: "docs",
routeBasePath: "/",
path: ".",
exclude: [...GlobExcludeDefault],
include: ["**/*.mdx", "**/*.md"],
sidebarPath: "./sidebar.mjs",
showLastUpdateTime: false,
editUrl: "https://github.com/goauthentik/authentik/edit/main/docs/",
//#region Docs Plugins
beforeDefaultRemarkPlugins: [
remarkLinkRewrite([
// ---
["/docs", ""],
["/api", "https://api.goauthentik.io"],
["/integrations", "https://integrations.goauthentik.io"],
]),
],
}),
),
],
],
};
export default /** @type {Config} */ (deepmerge(CommonConfig, createDocusaurusConfig(config)));

View File

@ -1,46 +0,0 @@
[[plugins]]
package = "netlify-plugin-cache"
[plugins.inputs]
paths = [".docusaurus", ".cache", 'node_modules/.cache']
[[plugins]]
package = "netlify-plugin-debug-cache"
[build]
base = "docs"
package = "topics"
command = "npm run build -w topics"
publish = "topics/build"
[dev]
command = "npm start"
targetPort = 3000
publish = "topics/build"
[context.production.environment]
NODE_ENV = "production"
[context.dev.environment]
NODE_ENV = "development"
# Migration from docs to separate directory
[[redirects]]
from = "/docs/integrations/*"
to = "https://integrations.goauthentik.io/:splat"
status = 302
[[redirects]]
from = "/integrations/*"
to = "https://integrations.goauthentik.io/:splat"
status = 302
[[redirects]]
from = "/docs/*"
to = "/:splat"
status = 302
[[headers]]
for = "/*"
[headers.values]
X-Frame-Options = "DENY"

View File

@ -1,22 +0,0 @@
{
"name": "@goauthentik/docs-topics",
"version": "0.0.0",
"license": "MIT",
"private": true,
"scripts": {
"build": "run-s build:types build:docusaurus",
"build:docusaurus": "docusaurus build",
"build:types": "tsc -b .",
"deploy": "docusaurus deploy",
"docusaurus": "docusaurus",
"serve": "docusaurus serve",
"start": "docusaurus start",
"test": "node --test"
},
"dependencies": {
"@goauthentik/docusaurus-theme": "*"
},
"engines": {
"node": ">=24"
}
}

View File

@ -1,8 +0,0 @@
{
"extends": "../tsconfig.base.json",
"references": [
{
"path": "../docusaurus-theme"
}
]
}

View File

@ -1,27 +0,0 @@
// @file TSConfig used by the docs package during development.
//
// @remarks
// While this configuration will influence the IDE experience,
// Docusaurus will instead use an internal configuration to build the site.
//
// @see https://docusaurus.io/docs/typescript-support
{
"extends": "./tsconfig.base.json",
"files": [],
"references": [
{
"path": "./docusaurus-theme"
},
{
"path": "./api"
},
{
"path": "./integrations"
},
{
"path": "./topics"
}
]
}

View File

@ -23,8 +23,8 @@ export const DefaultIgnorePatterns = [
"**/out",
"**/dist",
"**/.wireit",
"docs/**/build/**",
"docs/.docusaurus/**",
"website/build/**",
"website/.docusaurus/**",
"**/node_modules",
"**/coverage",
"**/storybook-static",

View File

@ -43,7 +43,7 @@ dependencies = [
"jwcrypto==1.5.6",
"kubernetes==33.1.0",
"ldap3==2.9.1",
"lxml==6.0.0",
"lxml==5.4.0",
"msgraph-sdk==1.35.0",
"opencontainers==0.0.14",
"packaging==25.0",
@ -144,9 +144,7 @@ skip = [
"**/web/out",
"./web/storybook-static",
"./web/custom-elements.json",
"./docs/build",
"./docs/api/build",
"./docs/integrations/build",
"./website/build",
"./gen-ts-api",
"./gen-py-api",
"./gen-go-api",
@ -201,7 +199,7 @@ omit = [
"*/migrations/*",
"*/management/commands/*",
"*/apps.py",
"docs/",
"website/",
]
[tool.coverage.report]

View File

@ -24864,6 +24864,7 @@ paths:
- authentik_policies_password.passwordpolicy
- authentik_policies_reputation.reputationpolicy
- authentik_policies_unique_password.uniquepasswordpolicy
- authentik_providers_apple_psso.appleplatformssoprovider
- authentik_providers_google_workspace.googleworkspaceprovider
- authentik_providers_google_workspace.googleworkspaceprovidermapping
- authentik_providers_ldap.ldapprovider
@ -25113,6 +25114,7 @@ paths:
- authentik_policies_password.passwordpolicy
- authentik_policies_reputation.reputationpolicy
- authentik_policies_unique_password.uniquepasswordpolicy
- authentik_providers_apple_psso.appleplatformssoprovider
- authentik_providers_google_workspace.googleworkspaceprovider
- authentik_providers_google_workspace.googleworkspaceprovidermapping
- authentik_providers_ldap.ldapprovider
@ -41212,6 +41214,7 @@ components:
- authentik.enterprise
- authentik.enterprise.audit
- authentik.enterprise.policies.unique_password
- authentik.enterprise.providers.apple_psso
- authentik.enterprise.providers.google_workspace
- authentik.enterprise.providers.microsoft_entra
- authentik.enterprise.providers.ssf
@ -41258,6 +41261,15 @@ components:
- redirect_uri
- scope
- state
ApplePlatformSSOProviderRequest:
type: object
description: ApplePlatformSSOProvider Serializer
properties:
name:
type: string
minLength: 1
required:
- name
Application:
type: object
description: Application Serializer
@ -48714,6 +48726,7 @@ components:
- authentik_core.token
- authentik_enterprise.license
- authentik_policies_unique_password.uniquepasswordpolicy
- authentik_providers_apple_psso.appleplatformssoprovider
- authentik_providers_google_workspace.googleworkspaceprovider
- authentik_providers_google_workspace.googleworkspaceprovidermapping
- authentik_providers_microsoft_entra.microsoftentraprovider
@ -56659,6 +56672,7 @@ components:
type: string
ProviderModelEnum:
enum:
- authentik_providers_apple_psso.appleplatformssoprovider
- authentik_providers_google_workspace.googleworkspaceprovider
- authentik_providers_ldap.ldapprovider
- authentik_providers_microsoft_entra.microsoftentraprovider
@ -61871,6 +61885,7 @@ components:
- worker_id
modelRequest:
oneOf:
- $ref: '#/components/schemas/ApplePlatformSSOProviderRequest'
- $ref: '#/components/schemas/GoogleWorkspaceProviderRequest'
- $ref: '#/components/schemas/LDAPProviderRequest'
- $ref: '#/components/schemas/MicrosoftEntraProviderRequest'
@ -61884,6 +61899,7 @@ components:
discriminator:
propertyName: provider_model
mapping:
authentik_providers_apple_psso.appleplatformssoprovider: '#/components/schemas/ApplePlatformSSOProviderRequest'
authentik_providers_google_workspace.googleworkspaceprovider: '#/components/schemas/GoogleWorkspaceProviderRequest'
authentik_providers_ldap.ldapprovider: '#/components/schemas/LDAPProviderRequest'
authentik_providers_microsoft_entra.microsoftentraprovider: '#/components/schemas/MicrosoftEntraProviderRequest'

35
uv.lock generated
View File

@ -305,7 +305,7 @@ requires-dist = [
{ name = "jwcrypto", specifier = "==1.5.6" },
{ name = "kubernetes", specifier = "==33.1.0" },
{ name = "ldap3", specifier = "==2.9.1" },
{ name = "lxml", specifier = "==6.0.0" },
{ name = "lxml", specifier = "==5.4.0" },
{ name = "msgraph-sdk", specifier = "==1.35.0" },
{ name = "opencontainers", git = "https://github.com/vsoch/oci-python?rev=ceb4fcc090851717a3069d78e85ceb1e86c2740c" },
{ name = "packaging", specifier = "==25.0" },
@ -1824,22 +1824,27 @@ wheels = [
[[package]]
name = "lxml"
version = "6.0.0"
version = "5.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c5/ed/60eb6fa2923602fba988d9ca7c5cdbd7cf25faa795162ed538b527a35411/lxml-6.0.0.tar.gz", hash = "sha256:032e65120339d44cdc3efc326c9f660f5f7205f3a535c1fdbf898b29ea01fb72", size = 4096938, upload-time = "2025-06-26T16:28:19.373Z" }
sdist = { url = "https://files.pythonhosted.org/packages/76/3d/14e82fc7c8fb1b7761f7e748fd47e2ec8276d137b6acfe5a4bb73853e08f/lxml-5.4.0.tar.gz", hash = "sha256:d12832e1dbea4be280b22fd0ea7c9b87f0d8fc51ba06e92dc62d52f804f78ebd", size = 3679479, upload-time = "2025-04-23T01:50:29.322Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/79/21/6e7c060822a3c954ff085e5e1b94b4a25757c06529eac91e550f3f5cd8b8/lxml-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6da7cd4f405fd7db56e51e96bff0865b9853ae70df0e6720624049da76bde2da", size = 8414372, upload-time = "2025-06-26T16:26:39.079Z" },
{ url = "https://files.pythonhosted.org/packages/a4/f6/051b1607a459db670fc3a244fa4f06f101a8adf86cda263d1a56b3a4f9d5/lxml-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b34339898bb556a2351a1830f88f751679f343eabf9cf05841c95b165152c9e7", size = 4593940, upload-time = "2025-06-26T16:26:41.891Z" },
{ url = "https://files.pythonhosted.org/packages/8e/74/dd595d92a40bda3c687d70d4487b2c7eff93fd63b568acd64fedd2ba00fe/lxml-6.0.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:51a5e4c61a4541bd1cd3ba74766d0c9b6c12d6a1a4964ef60026832aac8e79b3", size = 5214329, upload-time = "2025-06-26T16:26:44.669Z" },
{ url = "https://files.pythonhosted.org/packages/7c/4b/20555bdd75d57945bdabfbc45fdb1a36a1a0ff9eae4653e951b2b79c9209/lxml-6.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f4b481b6cc3a897adb4279216695150bbe7a44c03daba3c894f49d2037e0a24", size = 5021931, upload-time = "2025-06-26T16:26:47.503Z" },
{ url = "https://files.pythonhosted.org/packages/d4/dd/39c8507c16db6031f8c1ddf70ed95dbb0a6d466a40002a3522c128aba472/lxml-6.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae06fbab4f1bb7db4f7c8ca9897dc8db4447d1a2b9bee78474ad403437bcc29", size = 5247467, upload-time = "2025-06-26T16:26:49.998Z" },
{ url = "https://files.pythonhosted.org/packages/4d/56/732d49def0631ad633844cfb2664563c830173a98d5efd9b172e89a4800d/lxml-6.0.0-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:1fa377b827ca2023244a06554c6e7dc6828a10aaf74ca41965c5d8a4925aebb4", size = 4720601, upload-time = "2025-06-26T16:26:52.564Z" },
{ url = "https://files.pythonhosted.org/packages/8f/7f/6b956fab95fa73462bca25d1ea7fc8274ddf68fb8e60b78d56c03b65278e/lxml-6.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1676b56d48048a62ef77a250428d1f31f610763636e0784ba67a9740823988ca", size = 5060227, upload-time = "2025-06-26T16:26:55.054Z" },
{ url = "https://files.pythonhosted.org/packages/97/06/e851ac2924447e8b15a294855caf3d543424364a143c001014d22c8ca94c/lxml-6.0.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:0e32698462aacc5c1cf6bdfebc9c781821b7e74c79f13e5ffc8bfe27c42b1abf", size = 4790637, upload-time = "2025-06-26T16:26:57.384Z" },
{ url = "https://files.pythonhosted.org/packages/52/03/0e764ce00b95e008d76b99d432f1807f3574fb2945b496a17807a1645dbd/lxml-6.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7488a43033c958637b1a08cddc9188eb06d3ad36582cebc7d4815980b47e27ef", size = 5272430, upload-time = "2025-06-26T16:27:00.031Z" },
{ url = "https://files.pythonhosted.org/packages/5f/01/d48cc141bc47bc1644d20fe97bbd5e8afb30415ec94f146f2f76d0d9d098/lxml-6.0.0-cp313-cp313-win32.whl", hash = "sha256:5fcd7d3b1d8ecb91445bd71b9c88bdbeae528fefee4f379895becfc72298d181", size = 3612896, upload-time = "2025-06-26T16:27:04.251Z" },
{ url = "https://files.pythonhosted.org/packages/f4/87/6456b9541d186ee7d4cb53bf1b9a0d7f3b1068532676940fdd594ac90865/lxml-6.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:2f34687222b78fff795feeb799a7d44eca2477c3d9d3a46ce17d51a4f383e32e", size = 4013132, upload-time = "2025-06-26T16:27:06.415Z" },
{ url = "https://files.pythonhosted.org/packages/b7/42/85b3aa8f06ca0d24962f8100f001828e1f1f1a38c954c16e71154ed7d53a/lxml-6.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:21db1ec5525780fd07251636eb5f7acb84003e9382c72c18c542a87c416ade03", size = 3672642, upload-time = "2025-06-26T16:27:09.888Z" },
{ url = "https://files.pythonhosted.org/packages/87/cb/2ba1e9dd953415f58548506fa5549a7f373ae55e80c61c9041b7fd09a38a/lxml-5.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:773e27b62920199c6197130632c18fb7ead3257fce1ffb7d286912e56ddb79e0", size = 8110086, upload-time = "2025-04-23T01:46:52.218Z" },
{ url = "https://files.pythonhosted.org/packages/b5/3e/6602a4dca3ae344e8609914d6ab22e52ce42e3e1638c10967568c5c1450d/lxml-5.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ce9c671845de9699904b1e9df95acfe8dfc183f2310f163cdaa91a3535af95de", size = 4404613, upload-time = "2025-04-23T01:46:55.281Z" },
{ url = "https://files.pythonhosted.org/packages/4c/72/bf00988477d3bb452bef9436e45aeea82bb40cdfb4684b83c967c53909c7/lxml-5.4.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9454b8d8200ec99a224df8854786262b1bd6461f4280064c807303c642c05e76", size = 5012008, upload-time = "2025-04-23T01:46:57.817Z" },
{ url = "https://files.pythonhosted.org/packages/92/1f/93e42d93e9e7a44b2d3354c462cd784dbaaf350f7976b5d7c3f85d68d1b1/lxml-5.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cccd007d5c95279e529c146d095f1d39ac05139de26c098166c4beb9374b0f4d", size = 4760915, upload-time = "2025-04-23T01:47:00.745Z" },
{ url = "https://files.pythonhosted.org/packages/45/0b/363009390d0b461cf9976a499e83b68f792e4c32ecef092f3f9ef9c4ba54/lxml-5.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0fce1294a0497edb034cb416ad3e77ecc89b313cff7adbee5334e4dc0d11f422", size = 5283890, upload-time = "2025-04-23T01:47:04.702Z" },
{ url = "https://files.pythonhosted.org/packages/19/dc/6056c332f9378ab476c88e301e6549a0454dbee8f0ae16847414f0eccb74/lxml-5.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:24974f774f3a78ac12b95e3a20ef0931795ff04dbb16db81a90c37f589819551", size = 4812644, upload-time = "2025-04-23T01:47:07.833Z" },
{ url = "https://files.pythonhosted.org/packages/ee/8a/f8c66bbb23ecb9048a46a5ef9b495fd23f7543df642dabeebcb2eeb66592/lxml-5.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:497cab4d8254c2a90bf988f162ace2ddbfdd806fce3bda3f581b9d24c852e03c", size = 4921817, upload-time = "2025-04-23T01:47:10.317Z" },
{ url = "https://files.pythonhosted.org/packages/04/57/2e537083c3f381f83d05d9b176f0d838a9e8961f7ed8ddce3f0217179ce3/lxml-5.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:e794f698ae4c5084414efea0f5cc9f4ac562ec02d66e1484ff822ef97c2cadff", size = 4753916, upload-time = "2025-04-23T01:47:12.823Z" },
{ url = "https://files.pythonhosted.org/packages/d8/80/ea8c4072109a350848f1157ce83ccd9439601274035cd045ac31f47f3417/lxml-5.4.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:2c62891b1ea3094bb12097822b3d44b93fc6c325f2043c4d2736a8ff09e65f60", size = 5289274, upload-time = "2025-04-23T01:47:15.916Z" },
{ url = "https://files.pythonhosted.org/packages/b3/47/c4be287c48cdc304483457878a3f22999098b9a95f455e3c4bda7ec7fc72/lxml-5.4.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:142accb3e4d1edae4b392bd165a9abdee8a3c432a2cca193df995bc3886249c8", size = 4874757, upload-time = "2025-04-23T01:47:19.793Z" },
{ url = "https://files.pythonhosted.org/packages/2f/04/6ef935dc74e729932e39478e44d8cfe6a83550552eaa072b7c05f6f22488/lxml-5.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1a42b3a19346e5601d1b8296ff6ef3d76038058f311902edd574461e9c036982", size = 4947028, upload-time = "2025-04-23T01:47:22.401Z" },
{ url = "https://files.pythonhosted.org/packages/cb/f9/c33fc8daa373ef8a7daddb53175289024512b6619bc9de36d77dca3df44b/lxml-5.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4291d3c409a17febf817259cb37bc62cb7eb398bcc95c1356947e2871911ae61", size = 4834487, upload-time = "2025-04-23T01:47:25.513Z" },
{ url = "https://files.pythonhosted.org/packages/8d/30/fc92bb595bcb878311e01b418b57d13900f84c2b94f6eca9e5073ea756e6/lxml-5.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4f5322cf38fe0e21c2d73901abf68e6329dc02a4994e483adbcf92b568a09a54", size = 5381688, upload-time = "2025-04-23T01:47:28.454Z" },
{ url = "https://files.pythonhosted.org/packages/43/d1/3ba7bd978ce28bba8e3da2c2e9d5ae3f8f521ad3f0ca6ea4788d086ba00d/lxml-5.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0be91891bdb06ebe65122aa6bf3fc94489960cf7e03033c6f83a90863b23c58b", size = 5242043, upload-time = "2025-04-23T01:47:31.208Z" },
{ url = "https://files.pythonhosted.org/packages/ee/cd/95fa2201041a610c4d08ddaf31d43b98ecc4b1d74b1e7245b1abdab443cb/lxml-5.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:15a665ad90054a3d4f397bc40f73948d48e36e4c09f9bcffc7d90c87410e478a", size = 5021569, upload-time = "2025-04-23T01:47:33.805Z" },
{ url = "https://files.pythonhosted.org/packages/2d/a6/31da006fead660b9512d08d23d31e93ad3477dd47cc42e3285f143443176/lxml-5.4.0-cp313-cp313-win32.whl", hash = "sha256:d5663bc1b471c79f5c833cffbc9b87d7bf13f87e055a5c86c363ccd2348d7e82", size = 3485270, upload-time = "2025-04-23T01:47:36.133Z" },
{ url = "https://files.pythonhosted.org/packages/fc/14/c115516c62a7d2499781d2d3d7215218c0731b2c940753bf9f9b7b73924d/lxml-5.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:bcb7a1096b4b6b24ce1ac24d4942ad98f983cd3810f9711bcd0293f43a9d8b9f", size = 3814606, upload-time = "2025-04-23T01:47:39.028Z" },
]
[[package]]

View File

@ -39,10 +39,8 @@ const pluginName = "mdx-plugin";
* @returns {Plugin}
*/
export function mdxPlugin({ root }) {
const prefix = "~docs";
// TODO: Replace with `resolvePackage` after NPM Workspaces support is added.
const docsPackageRoot = path.resolve(MonoRepoRoot, "docs");
const docsPackageRoot = path.resolve(MonoRepoRoot, "website");
/**
* @param {PluginBuild} build
@ -56,8 +54,7 @@ export function mdxPlugin({ root }) {
if (!args.path.startsWith("~")) return args;
return {
path: path.join(docsPackageRoot, "topics", args.path.slice(prefix.length)),
path: path.resolve(docsPackageRoot, args.path.slice(1)),
pluginName,
};
}
@ -77,7 +74,7 @@ export function mdxPlugin({ root }) {
const publicPath = path.resolve(
"/",
path.relative(path.join(root, "docs", "topics"), data.path),
path.relative(path.join(root, "website"), data.path),
);
const publicDirectory = path.dirname(publicPath);

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