Compare commits
27 Commits
interfaces
...
outposts/f
Author | SHA1 | Date | |
---|---|---|---|
4e9a466d64 | |||
9bd8cfbac0 | |||
e18c2fe084 | |||
205f11532f | |||
bc6d66cd88 | |||
609e9a00b4 | |||
d5708d22e0 | |||
71ac1282f9 | |||
cf9d8f64a2 | |||
1cda01511b | |||
13591fc72c | |||
b604ff5114 | |||
f72fa41a75 | |||
adf4191066 | |||
d2de586cc9 | |||
dad5021870 | |||
ab3f993bb9 | |||
158fe2f9bb | |||
5970a6e2a2 | |||
5c8f024d12 | |||
428daa5323 | |||
4001af4d35 | |||
f1cec03dcf | |||
574ed72b95 | |||
480f5c2aac | |||
d4e502fdf5 | |||
05b2fb5ec1 |
@ -51,12 +51,14 @@ runs:
|
||||
version_family = ".".join(version.split(".")[:-1])
|
||||
safe_branch_name = branch_name.replace("refs/heads/", "").replace("/", "-")
|
||||
|
||||
sha = os.environ["GITHUB_SHA"] if not "${{ github.event.pull_request.head.sha }}" else "${{ github.event.pull_request.head.sha }}"
|
||||
|
||||
with open(os.environ["GITHUB_OUTPUT"], "a+", encoding="utf-8") as _output:
|
||||
print("branchName=%s" % branch_name, file=_output)
|
||||
print("branchNameContainer=%s" % safe_branch_name, file=_output)
|
||||
print("timestamp=%s" % int(time()), file=_output)
|
||||
print("sha=%s" % os.environ["GITHUB_SHA"], file=_output)
|
||||
print("shortHash=%s" % os.environ["GITHUB_SHA"][:7], file=_output)
|
||||
print("sha=%s" % sha, file=_output)
|
||||
print("shortHash=%s" % sha[:7], file=_output)
|
||||
print("shouldBuild=%s" % should_build, file=_output)
|
||||
print("version=%s" % version, file=_output)
|
||||
print("versionFamily=%s" % version_family, file=_output)
|
||||
|
2
.github/actions/setup/action.yml
vendored
@ -21,7 +21,7 @@ runs:
|
||||
python-version: "3.11"
|
||||
cache: "poetry"
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v3.1.0
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "20"
|
||||
cache: "npm"
|
||||
|
6
.github/actions/setup/docker-compose.yml
vendored
@ -2,8 +2,7 @@ version: "3.7"
|
||||
|
||||
services:
|
||||
postgresql:
|
||||
container_name: postgres
|
||||
image: library/postgres:${PSQL_TAG:-12}
|
||||
image: docker.io/library/postgres:${PSQL_TAG:-12}
|
||||
volumes:
|
||||
- db-data:/var/lib/postgresql/data
|
||||
environment:
|
||||
@ -14,8 +13,7 @@ services:
|
||||
- 5432:5432
|
||||
restart: always
|
||||
redis:
|
||||
container_name: redis
|
||||
image: library/redis
|
||||
image: docker.io/library/redis
|
||||
ports:
|
||||
- 6379:6379
|
||||
restart: always
|
||||
|
4
.github/workflows/ci-main.yml
vendored
@ -187,6 +187,8 @@ jobs:
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2.1.0
|
||||
- name: Set up Docker Buildx
|
||||
@ -229,6 +231,8 @@ jobs:
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2.1.0
|
||||
- name: Set up Docker Buildx
|
||||
|
5
.github/workflows/ci-outpost.yml
vendored
@ -30,6 +30,7 @@ jobs:
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
with:
|
||||
args: --timeout 5000s
|
||||
skip-pkg-cache: true
|
||||
test-unittest:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@ -63,6 +64,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2.1.0
|
||||
- name: Set up Docker Buildx
|
||||
@ -110,6 +113,8 @@ jobs:
|
||||
goarch: [amd64, arm64]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
|
28
.github/workflows/codeql-analysis.yml
vendored
@ -4,7 +4,6 @@ on:
|
||||
push:
|
||||
branches: [main, "*", next, version*]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [main]
|
||||
schedule:
|
||||
- cron: "30 6 * * 5"
|
||||
@ -22,39 +21,16 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: ["go", "javascript", "python"]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
||||
# Learn more:
|
||||
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
|
@ -84,6 +84,8 @@ RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends libxmlsec1-openssl libmaxminddb0 && \
|
||||
# Required for bootstrap & healtcheck
|
||||
apt-get install -y --no-install-recommends runit && \
|
||||
# Required for outposts
|
||||
apt-get install -y --no-install-recommends openssh-client && \
|
||||
pip install --no-cache-dir -r /requirements.txt && \
|
||||
apt-get remove --purge -y build-essential pkg-config libxmlsec1-dev && \
|
||||
apt-get autoremove --purge -y && \
|
||||
@ -91,8 +93,9 @@ RUN apt-get update && \
|
||||
rm -rf /tmp/* /var/lib/apt/lists/* /var/tmp/ && \
|
||||
adduser --system --no-create-home --uid 1000 --group --home /authentik authentik && \
|
||||
mkdir -p /certs /media /blueprints && \
|
||||
mkdir -p /authentik/.ssh && \
|
||||
chown authentik:authentik /certs /media /authentik/.ssh
|
||||
chown authentik:authentik /certs /media && \
|
||||
chmod g+w /etc/ssh/ssh_config.d/ && \
|
||||
chgrp authentik /etc/ssh/ssh_config.d/
|
||||
|
||||
COPY ./authentik/ /authentik
|
||||
COPY ./pyproject.toml /
|
||||
|
@ -57,10 +57,6 @@ def event_trigger_handler(event_uuid: str, trigger_name: str):
|
||||
LOGGER.debug("e(trigger): attempting to prevent infinite loop", trigger=trigger)
|
||||
return
|
||||
|
||||
if not trigger.group:
|
||||
LOGGER.debug("e(trigger): trigger has no group", trigger=trigger)
|
||||
return
|
||||
|
||||
LOGGER.debug("e(trigger): checking if trigger applies", trigger=trigger)
|
||||
try:
|
||||
user = User.objects.filter(pk=event.user.get("pk")).first() or get_anonymous_user()
|
||||
@ -77,6 +73,10 @@ def event_trigger_handler(event_uuid: str, trigger_name: str):
|
||||
if not result.passing:
|
||||
return
|
||||
|
||||
if not trigger.group:
|
||||
LOGGER.debug("e(trigger): trigger has no group", trigger=trigger)
|
||||
return
|
||||
|
||||
LOGGER.debug("e(trigger): event trigger matched", trigger=trigger)
|
||||
# Create the notification objects
|
||||
for transport in trigger.transports.all():
|
||||
|
@ -67,7 +67,7 @@ def sentry_init(**sentry_init_kwargs):
|
||||
ArgvIntegration(),
|
||||
StdlibIntegration(),
|
||||
DjangoIntegration(transaction_style="function_name"),
|
||||
CeleryIntegration(monitor_beat_tasks=True),
|
||||
CeleryIntegration(),
|
||||
RedisIntegration(),
|
||||
ThreadingIntegration(propagate_hub=True),
|
||||
SocketIntegration(),
|
||||
|
@ -1,4 +1,5 @@
|
||||
"""Docker controller"""
|
||||
from subprocess import SubprocessError # nosec
|
||||
from time import sleep
|
||||
from typing import Optional
|
||||
from urllib.parse import urlparse
|
||||
@ -9,7 +10,6 @@ from docker import DockerClient as UpstreamDockerClient
|
||||
from docker.errors import DockerException, NotFound
|
||||
from docker.models.containers import Container
|
||||
from docker.utils.utils import kwargs_from_env
|
||||
from paramiko.ssh_exception import SSHException
|
||||
from structlog.stdlib import get_logger
|
||||
from yaml import safe_dump
|
||||
|
||||
@ -58,8 +58,9 @@ class DockerClient(UpstreamDockerClient, BaseClient):
|
||||
super().__init__(
|
||||
base_url=connection.url,
|
||||
tls=tls_config,
|
||||
use_ssh_client=True,
|
||||
)
|
||||
except SSHException as exc:
|
||||
except SubprocessError as exc:
|
||||
if self.ssh:
|
||||
self.ssh.cleanup()
|
||||
raise ServiceConnectionInvalid(exc) from exc
|
||||
|
@ -7,8 +7,7 @@ from docker.errors import DockerException
|
||||
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
|
||||
HEADER = "### Managed by authentik"
|
||||
FOOTER = "### End Managed by authentik"
|
||||
SSH_CONFIG_DIR = Path("/etc/ssh/ssh_config.d/")
|
||||
|
||||
|
||||
def opener(path, flags):
|
||||
@ -28,70 +27,54 @@ class DockerInlineSSH:
|
||||
|
||||
key_path: str
|
||||
config_path: Path
|
||||
header: str
|
||||
|
||||
def __init__(self, host: str, keypair: CertificateKeyPair) -> None:
|
||||
self.host = host
|
||||
self.keypair = keypair
|
||||
self.config_path = Path("~/.ssh/config").expanduser()
|
||||
if self.config_path.exists() and HEADER not in self.config_path.read_text(encoding="utf-8"):
|
||||
# SSH Config file already exists and there's no header from us, meaning that it's
|
||||
# been externally mapped into the container for more complex configs
|
||||
raise SSHManagedExternallyException(
|
||||
"SSH Config exists and does not contain authentik header"
|
||||
)
|
||||
self.config_path = SSH_CONFIG_DIR / Path(self.host + ".conf")
|
||||
with open(self.config_path, "w", encoding="utf-8") as _config:
|
||||
if not _config.writable():
|
||||
# SSH Config file already exists and there's no header from us, meaning that it's
|
||||
# been externally mapped into the container for more complex configs
|
||||
raise SSHManagedExternallyException(
|
||||
"SSH Config exists and does not contain authentik header"
|
||||
)
|
||||
if not self.keypair:
|
||||
raise DockerException("keypair must be set for SSH connections")
|
||||
self.header = f"{HEADER} - {self.host}\n"
|
||||
|
||||
def write_config(self, key_path: str) -> bool:
|
||||
def write_config(self, key_path: str):
|
||||
"""Update the local user's ssh config file"""
|
||||
with open(self.config_path, "a+", encoding="utf-8") as ssh_config:
|
||||
if self.header in ssh_config.readlines():
|
||||
return False
|
||||
with open(self.config_path, "w", encoding="utf-8") as ssh_config:
|
||||
ssh_config.writelines(
|
||||
[
|
||||
self.header,
|
||||
f"Host {self.host}\n",
|
||||
f" IdentityFile {key_path}\n",
|
||||
f" IdentityFile {str(key_path)}\n",
|
||||
" StrictHostKeyChecking No\n",
|
||||
" UserKnownHostsFile /dev/null\n",
|
||||
f"{FOOTER}\n",
|
||||
"\n",
|
||||
]
|
||||
)
|
||||
return True
|
||||
|
||||
def write_key(self):
|
||||
def write_key(self) -> Path:
|
||||
"""Write keypair's private key to a temporary file"""
|
||||
path = Path(gettempdir(), f"{self.keypair.pk}_private.pem")
|
||||
with open(path, "w", encoding="utf8", opener=opener) as _file:
|
||||
_file.write(self.keypair.key_data)
|
||||
return str(path)
|
||||
return path
|
||||
|
||||
def write(self):
|
||||
"""Write keyfile and update ssh config"""
|
||||
self.key_path = self.write_key()
|
||||
was_written = self.write_config(self.key_path)
|
||||
if not was_written:
|
||||
try:
|
||||
self.write_config(self.key_path)
|
||||
except OSError:
|
||||
self.cleanup()
|
||||
|
||||
def cleanup(self):
|
||||
"""Cleanup when we're done"""
|
||||
try:
|
||||
os.unlink(self.key_path)
|
||||
with open(self.config_path, "r", encoding="utf-8") as ssh_config:
|
||||
start = 0
|
||||
end = 0
|
||||
lines = ssh_config.readlines()
|
||||
for idx, line in enumerate(lines):
|
||||
if line == self.header:
|
||||
start = idx
|
||||
if start != 0 and line == f"{FOOTER}\n":
|
||||
end = idx
|
||||
with open(self.config_path, "w+", encoding="utf-8") as ssh_config:
|
||||
lines = lines[:start] + lines[end + 2 :]
|
||||
ssh_config.writelines(lines)
|
||||
os.unlink(self.config_path)
|
||||
except OSError:
|
||||
# If we fail deleting a file it doesn't matter that much
|
||||
# since we're just in a container
|
||||
|
@ -1,5 +1,5 @@
|
||||
---
|
||||
version: '3.4'
|
||||
version: "3.4"
|
||||
|
||||
services:
|
||||
postgresql:
|
||||
@ -14,9 +14,9 @@ services:
|
||||
volumes:
|
||||
- database:/var/lib/postgresql/data
|
||||
environment:
|
||||
- POSTGRES_PASSWORD=${PG_PASS:?database password required}
|
||||
- POSTGRES_USER=${PG_USER:-authentik}
|
||||
- POSTGRES_DB=${PG_DB:-authentik}
|
||||
POSTGRES_PASSWORD: ${PG_PASS:?database password required}
|
||||
POSTGRES_USER: ${PG_USER:-authentik}
|
||||
POSTGRES_DB: ${PG_DB:-authentik}
|
||||
env_file:
|
||||
- .env
|
||||
redis:
|
||||
@ -47,8 +47,8 @@ services:
|
||||
env_file:
|
||||
- .env
|
||||
ports:
|
||||
- "${AUTHENTIK_PORT_HTTP:-9000}:9000"
|
||||
- "${AUTHENTIK_PORT_HTTPS:-9443}:9443"
|
||||
- "${COMPOSE_PORT_HTTP:-9000}:9000"
|
||||
- "${COMPOSE_PORT_HTTPS:-9443}:9443"
|
||||
worker:
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.4.1}
|
||||
restart: unless-stopped
|
||||
|
@ -30,7 +30,7 @@ type RedisConfig struct {
|
||||
Password string `yaml:"password" env:"AUTHENTIK_REDIS__PASSWORD"`
|
||||
TLS bool `yaml:"tls" env:"AUTHENTIK_REDIS__TLS"`
|
||||
TLSReqs string `yaml:"tls_reqs" env:"AUTHENTIK_REDIS__TLS_REQS"`
|
||||
DB int `yaml:"cache_db" env:"AUTHENTIK_REDIS__CACHE_DB"`
|
||||
DB int `yaml:"cache_db" env:"AUTHENTIK_REDIS__DB"`
|
||||
CacheTimeout int `yaml:"cache_timeout" env:"AUTHENTIK_REDIS__CACHE_TIMEOUT"`
|
||||
CacheTimeoutFlows int `yaml:"cache_timeout_flows" env:"AUTHENTIK_REDIS__CACHE_TIMEOUT_FLOWS"`
|
||||
CacheTimeoutPolicies int `yaml:"cache_timeout_policies" env:"AUTHENTIK_REDIS__CACHE_TIMEOUT_POLICIES"`
|
||||
|
17
poetry.lock
generated
@ -1676,14 +1676,14 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "importlib-metadata"
|
||||
version = "6.5.0"
|
||||
version = "6.6.0"
|
||||
description = "Read metadata from Python packages"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "importlib_metadata-6.5.0-py3-none-any.whl", hash = "sha256:03ba783c3a2c69d751b109fc0c94a62c51f581b3d6acf8ed1331b6d5729321ff"},
|
||||
{file = "importlib_metadata-6.5.0.tar.gz", hash = "sha256:7a8bdf1bc3a726297f5cfbc999e6e7ff6b4fa41b26bba4afc580448624460045"},
|
||||
{file = "importlib_metadata-6.6.0-py3-none-any.whl", hash = "sha256:43dd286a2cd8995d5eaef7fee2066340423b818ed3fd70adf0bad5f1fac53fed"},
|
||||
{file = "importlib_metadata-6.6.0.tar.gz", hash = "sha256:92501cdf9cc66ebd3e612f1b4f0c0765dfa42f0fa38ffb319b6bd84dd675d705"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -3263,16 +3263,21 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlparse"
|
||||
version = "0.4.3"
|
||||
version = "0.4.4"
|
||||
description = "A non-validating SQL parser."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
files = [
|
||||
{file = "sqlparse-0.4.3-py3-none-any.whl", hash = "sha256:0323c0ec29cd52bceabc1b4d9d579e311f3e4961b98d174201d5622a23b85e34"},
|
||||
{file = "sqlparse-0.4.3.tar.gz", hash = "sha256:69ca804846bb114d2ec380e4360a8a340db83f0ccf3afceeb1404df028f57268"},
|
||||
{file = "sqlparse-0.4.4-py3-none-any.whl", hash = "sha256:5430a4fe2ac7d0f93e66f1efc6e1338a41884b7ddf2a350cedd20ccc4d9d28f3"},
|
||||
{file = "sqlparse-0.4.4.tar.gz", hash = "sha256:d446183e84b8349fa3061f0fe7f06ca94ba65b426946ffebe6e3e8295332420c"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
dev = ["build", "flake8"]
|
||||
doc = ["sphinx"]
|
||||
test = ["pytest", "pytest-cov"]
|
||||
|
||||
[[package]]
|
||||
name = "stevedore"
|
||||
version = "4.1.1"
|
||||
|
@ -3,6 +3,7 @@ import json
|
||||
import os
|
||||
from functools import lru_cache, wraps
|
||||
from os import environ
|
||||
from sys import stderr
|
||||
from time import sleep
|
||||
from typing import Any, Callable, Optional
|
||||
|
||||
@ -28,6 +29,7 @@ from authentik.core.models import User
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
|
||||
RETRIES = int(environ.get("RETRIES", "3"))
|
||||
IS_CI = "CI" in environ
|
||||
|
||||
|
||||
def get_docker_tag() -> str:
|
||||
@ -49,6 +51,8 @@ class SeleniumTestCase(StaticLiveServerTestCase):
|
||||
user: User
|
||||
|
||||
def setUp(self):
|
||||
if IS_CI:
|
||||
print("::group::authentik Logs", file=stderr)
|
||||
super().setUp()
|
||||
# pylint: disable=invalid-name
|
||||
self.maxDiff = None
|
||||
@ -91,8 +95,12 @@ class SeleniumTestCase(StaticLiveServerTestCase):
|
||||
def output_container_logs(self, container: Optional[Container] = None):
|
||||
"""Output the container logs to our STDOUT"""
|
||||
_container = container or self.container
|
||||
if IS_CI:
|
||||
print(f"::group::Container logs - {_container.image.tags[0]}")
|
||||
for log in _container.logs().decode().split("\n"):
|
||||
self.logger.info(log, source="container", container=_container.image.tags[0])
|
||||
print(log)
|
||||
if IS_CI:
|
||||
print("::endgroup::")
|
||||
|
||||
def get_container_specs(self) -> Optional[dict[str, Any]]:
|
||||
"""Optionally get container specs which will launched on setup, wait for the container to
|
||||
@ -118,15 +126,19 @@ class SeleniumTestCase(StaticLiveServerTestCase):
|
||||
raise ValueError(f"Webdriver failed after {RETRIES}.")
|
||||
|
||||
def tearDown(self):
|
||||
self.logger.debug("--------browser logs")
|
||||
super().tearDown()
|
||||
if IS_CI:
|
||||
print("::endgroup::", file=stderr)
|
||||
if IS_CI:
|
||||
print("::group::Browser logs")
|
||||
for line in self.driver.get_log("browser"):
|
||||
self.logger.debug(line["message"], source=line["source"], level=line["level"])
|
||||
self.logger.debug("--------end browser logs")
|
||||
print(line["message"])
|
||||
if IS_CI:
|
||||
print("::endgroup::")
|
||||
if self.container:
|
||||
self.output_container_logs()
|
||||
self.container.kill()
|
||||
self.driver.quit()
|
||||
super().tearDown()
|
||||
|
||||
def wait_for_url(self, desired_url):
|
||||
"""Wait until URL is `desired_url`."""
|
||||
|
35
web/package-lock.json
generated
@ -54,7 +54,7 @@
|
||||
"construct-style-sheets-polyfill": "^3.1.0",
|
||||
"core-js": "^3.30.1",
|
||||
"country-flag-icons": "^1.5.7",
|
||||
"eslint": "^8.38.0",
|
||||
"eslint": "^8.39.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-plugin-custom-elements": "0.0.8",
|
||||
"eslint-plugin-lit": "^1.8.3",
|
||||
@ -62,7 +62,7 @@
|
||||
"lit": "^2.7.2",
|
||||
"mermaid": "^10.1.0",
|
||||
"moment": "^2.29.4",
|
||||
"prettier": "^2.8.7",
|
||||
"prettier": "^2.8.8",
|
||||
"pyright": "^1.1.304",
|
||||
"rapidoc": "^9.3.4",
|
||||
"rollup": "^2.79.1",
|
||||
@ -1982,9 +1982,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/js": {
|
||||
"version": "8.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.38.0.tgz",
|
||||
"integrity": "sha512-IoD2MfUnOV58ghIHCiil01PcohxjbYR/qCxsoC+xNgUwh1EY8jOOrYmu3d3a71+tJJ23uscEV4X2HJWMsPJu4g==",
|
||||
"version": "8.39.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.39.0.tgz",
|
||||
"integrity": "sha512-kf9RB0Fg7NZfap83B3QOqOGg9QmD9yBudqQXzzOtn3i4y7ZUXe5ONeW34Gwi+TxhH4mvj72R1Zc300KUMa9Bng==",
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
}
|
||||
@ -5255,14 +5255,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "8.38.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.38.0.tgz",
|
||||
"integrity": "sha512-pIdsD2jwlUGf/U38Jv97t8lq6HpaU/G9NKbYmpWpZGw3LdTNhZLbJePqxOXGB5+JEKfOPU/XLxYxFh03nr1KTg==",
|
||||
"version": "8.39.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.39.0.tgz",
|
||||
"integrity": "sha512-mwiok6cy7KTW7rBpo05k6+p4YVZByLNjAZ/ACB9DRCu4YDRwjXI01tWHp6KAUWelsBetTxKK/2sHB0vdS8Z2Og==",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.4.0",
|
||||
"@eslint/eslintrc": "^2.0.2",
|
||||
"@eslint/js": "8.38.0",
|
||||
"@eslint/js": "8.39.0",
|
||||
"@humanwhocodes/config-array": "^0.11.8",
|
||||
"@humanwhocodes/module-importer": "^1.0.1",
|
||||
"@nodelib/fs.walk": "^1.2.8",
|
||||
@ -5272,7 +5272,7 @@
|
||||
"debug": "^4.3.2",
|
||||
"doctrine": "^3.0.0",
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
"eslint-scope": "^7.1.1",
|
||||
"eslint-scope": "^7.2.0",
|
||||
"eslint-visitor-keys": "^3.4.0",
|
||||
"espree": "^9.5.1",
|
||||
"esquery": "^1.4.2",
|
||||
@ -5425,15 +5425,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/eslint-scope": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz",
|
||||
"integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==",
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz",
|
||||
"integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==",
|
||||
"dependencies": {
|
||||
"esrecurse": "^4.3.0",
|
||||
"estraverse": "^5.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/estraverse": {
|
||||
@ -8117,9 +8120,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "2.8.7",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.7.tgz",
|
||||
"integrity": "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==",
|
||||
"version": "2.8.8",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
|
||||
"integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
|
||||
"bin": {
|
||||
"prettier": "bin-prettier.js"
|
||||
},
|
||||
|
@ -98,7 +98,7 @@
|
||||
"construct-style-sheets-polyfill": "^3.1.0",
|
||||
"core-js": "^3.30.1",
|
||||
"country-flag-icons": "^1.5.7",
|
||||
"eslint": "^8.38.0",
|
||||
"eslint": "^8.39.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-plugin-custom-elements": "0.0.8",
|
||||
"eslint-plugin-lit": "^1.8.3",
|
||||
@ -106,7 +106,7 @@
|
||||
"lit": "^2.7.2",
|
||||
"mermaid": "^10.1.0",
|
||||
"moment": "^2.29.4",
|
||||
"prettier": "^2.8.7",
|
||||
"prettier": "^2.8.8",
|
||||
"pyright": "^1.1.304",
|
||||
"rapidoc": "^9.3.4",
|
||||
"rollup": "^2.79.1",
|
||||
|
@ -95,17 +95,25 @@ export class CertificateKeyPairListPage extends TablePage<CertificateKeyPair> {
|
||||
if (item.managed && item.managed.startsWith("goauthentik.io/crypto/discovered")) {
|
||||
managedSubText = t`Managed by authentik (Discovered)`;
|
||||
}
|
||||
let color = PFColor.Green;
|
||||
if (item.certExpiry) {
|
||||
const now = new Date();
|
||||
const inAMonth = new Date();
|
||||
inAMonth.setDate(inAMonth.getDate() + 30);
|
||||
if (item.certExpiry <= inAMonth) {
|
||||
color = PFColor.Orange;
|
||||
}
|
||||
if (item.certExpiry <= now) {
|
||||
color = PFColor.Red;
|
||||
}
|
||||
}
|
||||
return [
|
||||
html`<div>${item.name}</div>
|
||||
${item.managed ? html`<small>${managedSubText}</small>` : html``}`,
|
||||
html`<ak-label color=${item.privateKeyAvailable ? PFColor.Green : PFColor.Grey}>
|
||||
${item.privateKeyAvailable ? t`Yes (${item.privateKeyType?.toUpperCase()})` : t`No`}
|
||||
</ak-label>`,
|
||||
html`<ak-label
|
||||
color=${item.certExpiry || new Date() > new Date() ? PFColor.Green : PFColor.Orange}
|
||||
>
|
||||
${item.certExpiry?.toLocaleString()}
|
||||
</ak-label>`,
|
||||
html`<ak-label color=${color}> ${item.certExpiry?.toLocaleString()} </ak-label>`,
|
||||
html`<ak-forms-modal>
|
||||
<span slot="submit"> ${t`Update`} </span>
|
||||
<span slot="header"> ${t`Update Certificate-Key Pair`} </span>
|
||||
|
@ -50,9 +50,6 @@ export class FlowListPage extends TablePage<Flow> {
|
||||
|
||||
groupBy(items: Flow[]): [string, Flow[]][] {
|
||||
return groupBy(items, (flow) => {
|
||||
if (!flow.designation) {
|
||||
return "";
|
||||
}
|
||||
return DesignationToLabel(flow.designation);
|
||||
});
|
||||
}
|
||||
|
@ -85,7 +85,7 @@ export class OutpostServiceConnectionListPage extends TablePage<ServiceConnectio
|
||||
html`<ak-label color=${item.local ? PFColor.Grey : PFColor.Green}>
|
||||
${item.local ? t`Yes` : t`No`}
|
||||
</ak-label>`,
|
||||
html`${itemState.healthy
|
||||
html`${itemState?.healthy
|
||||
? html`<ak-label color=${PFColor.Green}>${ifDefined(itemState.version)}</ak-label>`
|
||||
: html`<ak-label color=${PFColor.Red}>${t`Unhealthy`}</ak-label>`}`,
|
||||
html` <ak-forms-modal>
|
||||
|
@ -8,7 +8,6 @@ import "@goauthentik/admin/policies/password/PasswordPolicyForm";
|
||||
import "@goauthentik/admin/policies/reputation/ReputationPolicyForm";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { uiConfig } from "@goauthentik/common/ui/config";
|
||||
import { groupBy } from "@goauthentik/common/utils";
|
||||
import { PFColor } from "@goauthentik/elements/Label";
|
||||
import "@goauthentik/elements/forms/ConfirmationForm";
|
||||
import "@goauthentik/elements/forms/DeleteBulkForm";
|
||||
@ -63,10 +62,6 @@ export class PolicyListPage extends TablePage<Policy> {
|
||||
];
|
||||
}
|
||||
|
||||
groupBy(items: Policy[]): [string, Policy[]][] {
|
||||
return groupBy(items, (policy) => policy.verboseNamePlural);
|
||||
}
|
||||
|
||||
row(item: Policy): TemplateResult[] {
|
||||
return [
|
||||
html`<div>${item.name}</div>
|
||||
|
@ -7,7 +7,6 @@ import "@goauthentik/admin/property-mappings/PropertyMappingTestForm";
|
||||
import "@goauthentik/admin/property-mappings/PropertyMappingWizard";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { uiConfig } from "@goauthentik/common/ui/config";
|
||||
import { groupBy } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/elements/forms/DeleteBulkForm";
|
||||
import "@goauthentik/elements/forms/ModalForm";
|
||||
import "@goauthentik/elements/forms/ProxyForm";
|
||||
@ -57,10 +56,6 @@ export class PropertyMappingListPage extends TablePage<PropertyMapping> {
|
||||
});
|
||||
}
|
||||
|
||||
groupBy(items: PropertyMapping[]): [string, PropertyMapping[]][] {
|
||||
return groupBy(items, (mapping) => mapping.verboseNamePlural);
|
||||
}
|
||||
|
||||
columns(): TableColumn[] {
|
||||
return [
|
||||
new TableColumn(t`Name`, "name"),
|
||||
|
@ -21,7 +21,6 @@ import "@goauthentik/admin/stages/user_logout/UserLogoutStageForm";
|
||||
import "@goauthentik/admin/stages/user_write/UserWriteStageForm";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { uiConfig } from "@goauthentik/common/ui/config";
|
||||
import { groupBy } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/elements/forms/DeleteBulkForm";
|
||||
import "@goauthentik/elements/forms/ModalForm";
|
||||
import "@goauthentik/elements/forms/ProxyForm";
|
||||
@ -66,10 +65,6 @@ export class StageListPage extends TablePage<Stage> {
|
||||
});
|
||||
}
|
||||
|
||||
groupBy(items: Stage[]): [string, Stage[]][] {
|
||||
return groupBy(items, (stage) => stage.verboseNamePlural);
|
||||
}
|
||||
|
||||
columns(): TableColumn[] {
|
||||
return [
|
||||
new TableColumn(t`Name`, "name"),
|
||||
|
@ -8,7 +8,7 @@ slug: /terminology
|
||||
graph LR
|
||||
source_ldap((LDAP Source)) <-->|Synchronizes| datasource_ldap["FreeIPA/
|
||||
Active Directory"]
|
||||
datasource_oauth1(Twtitter) --> source_oauth((OAuth/SAML\nSource))
|
||||
datasource_oauth1(Twitter) --> source_oauth((OAuth/SAML\nSource))
|
||||
datasource_oauth2(GitHub) --> source_oauth((OAuth/SAML\nSource))
|
||||
source_oauth --> authentik_db(authentik Database)
|
||||
source_ldap --> authentik_db(authentik Database)
|
||||
|
@ -31,6 +31,10 @@ After you've created the policies to match the events you want, create a "Notifi
|
||||
|
||||
You have to select which group the generated notification should be sent to. If left empty, the rule will be disabled.
|
||||
|
||||
:::info
|
||||
Before authentik 2023.5, when no group is selected, policies bound to the rule are not executed. Starting with authentik 2023.5, policies are executed even when no group is selected.
|
||||
:::
|
||||
|
||||
You also have to select which transports should be used to send the notification.
|
||||
A transport with the name "default-email-transport" is created by default. This transport will use the [global email configuration](../installation/docker-compose#email-configuration-optional-but-recommended).
|
||||
|
||||
|
@ -17,7 +17,7 @@ To disable these outbound connections, set the following in your `.env` file:
|
||||
AUTHENTIK_DISABLE_UPDATE_CHECK=true
|
||||
AUTHENTIK_ERROR_REPORTING__ENABLED=false
|
||||
AUTHENTIK_DISABLE_STARTUP_ANALYTICS=true
|
||||
AUTHENTIK_AVATARS=none
|
||||
AUTHENTIK_AVATARS=initials
|
||||
```
|
||||
|
||||
For a Helm-based install, set the following in your values.yaml file:
|
||||
|
@ -43,6 +43,8 @@ kubectl exec -it deployment/authentik-worker -c authentik -- ak dump_config
|
||||
- `AUTHENTIK_REDIS__HOST`: Hostname of your Redis Server
|
||||
- `AUTHENTIK_REDIS__PORT`: Redis port, defaults to 6379
|
||||
- `AUTHENTIK_REDIS__PASSWORD`: Password for your Redis Server
|
||||
- `AUTHENTIK_REDIS__TLS`: Use TLS to connect to Redis, defaults to false
|
||||
- `AUTHENTIK_REDIS__TLS_REQS`: Redis TLS requirements, defaults to "none"
|
||||
- `AUTHENTIK_REDIS__DB`: Database, defaults to 0
|
||||
- `AUTHENTIK_REDIS__CACHE_TIMEOUT`: Timeout for cached data until it expires in seconds, defaults to 300
|
||||
- `AUTHENTIK_REDIS__CACHE_TIMEOUT_FLOWS`: Timeout for cached flow plans until they expire in seconds, defaults to 300
|
||||
|
@ -60,14 +60,14 @@ AUTHENTIK_EMAIL__FROM=authentik@localhost
|
||||
|
||||
## Configure for port 80/443
|
||||
|
||||
By default, authentik listens on port 9000 for HTTP and 9443 for HTTPS. To change the default and instead use ports 80 and 443, you can set the following variables in `.env`:
|
||||
By default, authentik listens internally on port 9000 for HTTP and 9443 for HTTPS. To change the exposed ports to 80 and 443, you can set the following variables in `.env`:
|
||||
|
||||
```shell
|
||||
AUTHENTIK_PORT_HTTP=80
|
||||
AUTHENTIK_PORT_HTTPS=443
|
||||
COMPOSE_PORT_HTTP=80
|
||||
COMPOSE_PORT_HTTPS=443
|
||||
```
|
||||
|
||||
Be sure to run `docker-compose up -d` to rebuild with the new port numbers.
|
||||
See [Configuration](../installation/configuration) to change the internal ports. Be sure to run `docker-compose up -d` to rebuild with the new port numbers.
|
||||
|
||||
## Startup
|
||||
|
||||
|
@ -11,6 +11,10 @@ slug: "/releases/2023.5"
|
||||
|
||||
Additionally, any custom fields based on user attributes will only be represented with their sanitized key, removing any slashes with dashes, and removing periods.
|
||||
|
||||
- Renamed docker-compose environment variables
|
||||
|
||||
To better distinguish settings that configure authentik itself and settings that configure docker-compose, the environment variables `AUTHENTIK_PORT_HTTP` and `AUTHENTIK_PORT_HTTPS` have been renamed to `COMPOSE_PORT_HTTP` and `COMPOSE_PORT_HTTPS` respectively.
|
||||
|
||||
## New features
|
||||
|
||||
## Upgrading
|
||||
|
After Width: | Height: | Size: 92 KiB |
68
website/integrations/services/dokuwiki/index.md
Normal file
@ -0,0 +1,68 @@
|
||||
---
|
||||
title: DokuWiki
|
||||
---
|
||||
|
||||
<span class="badge badge--secondary">Support level: Community</span>
|
||||
|
||||
## What is Service Name
|
||||
|
||||
From https://en.wikipedia.org/wiki/DokuWiki
|
||||
|
||||
:::note
|
||||
DokuWiki is a wiki application licensed under GPLv2 and written in the PHP programming language. It works on plain text files and thus does not need a database. Its syntax is similar to the one used by MediaWiki. It is often recommended as a more lightweight, easier to customize alternative to MediaWiki.
|
||||
:::
|
||||
|
||||
## Preparation
|
||||
|
||||
The following placeholders will be used:
|
||||
|
||||
- `dokuwiki.company` is the FQDN of the DokiWiki install.
|
||||
- `authentik.company` is the FQDN of the authentik install.
|
||||
|
||||
## Service Configuration
|
||||
|
||||
In DokuWiki, navigate to the _Extension Manager_ section in the _Administration_ interface and install
|
||||
|
||||
- https://www.dokuwiki.org/plugin:oauth
|
||||
- https://www.dokuwiki.org/plugin:oauthgeneric
|
||||
|
||||
Navigate to _Configuration Settings_ section in the _Administration_ interface and change _Oauth_ and _Oauthgeneric_ options:
|
||||
|
||||
For _Oauth_:
|
||||
|
||||
- Check the _plugin»oauth»register-on-auth_ option
|
||||
|
||||
For _Oauthgeneric_:
|
||||
|
||||
- plugin»oauthgeneric»key: The Application UID
|
||||
- plugin»oauthgeneric»secret: The Application Secret
|
||||
- plugin»oauthgeneric»authurl: https://authentik.company/application/o/authorize/
|
||||
- plugin»oauthgeneric»tokenurl: https://authentik.company/application/o/token/
|
||||
- plugin»oauthgeneric»userurl: https://authentik.company/application/o/userinfo/
|
||||
- plugin»oauthgeneric»authmethod: Bearer Header
|
||||
- plugin»oauthgeneric»scopes: email, openid, profile
|
||||
- plugin»oauthgeneric»needs-state: checked
|
||||
- plugin»oauthgeneric»json-user: preferred_username
|
||||
- plugin»oauthgeneric»json-name: name
|
||||
- plugin»oauthgeneric»json-mail: email
|
||||
- plugin»oauthgeneric»json-grps: groups
|
||||
|
||||

|
||||
|
||||
In the _Configuration Settings_ section in the _Administration_ interface navigate to _Authentication_ and activate _oauth_ in _Authentication backend_.
|
||||
|
||||
## authentik Configuration
|
||||
|
||||
### Provider
|
||||
|
||||
In authentik, under _Providers_, create an _OAuth2/OpenID Provider_ with these settings:
|
||||
|
||||
- Redirect URI: The _Callback URL / Redirect URI_ from _plugin»oauth»info_, usually `dokuwiki.company/doku.php`
|
||||
- Signing Key: Select any available key
|
||||
|
||||
Note the _client ID_ and _client secret_, then save the provider. If you need to retrieve these values, you can do so by editing the provider.
|
||||
|
||||
### Application
|
||||
|
||||
In authentik, create an application which uses this provider. Optionally apply access restrictions to the application using policy bindings.
|
||||
Set the Launch URL to the _Callback URL / Redirect URI_ (`dokuwiki.company/doku.php`).
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 68 KiB |
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 60 KiB |
@ -28,11 +28,11 @@ The following placeholders will be used:
|
||||
|
||||
5. **Click to Reveal** the Client Secret and _save it for later_
|
||||
|
||||
6. Click **Add Redirect** and add https://authentik.company/source/oauth/callback/discord
|
||||
6. Click **Add Redirect** and add https://authentik.company/source/oauth/callback/discord/
|
||||
|
||||
Here is an example of a completed OAuth2 screen for Discord.
|
||||
|
||||

|
||||

|
||||
|
||||
## authentik
|
||||
|
||||
@ -45,7 +45,7 @@ Here is an example of a completed OAuth2 screen for Discord.
|
||||
|
||||
Here is an example of a complete authentik Discord OAuth Source
|
||||
|
||||

|
||||

|
||||
|
||||
Save, and you now have Discord as a source.
|
||||
|
||||
|
14
website/package-lock.json
generated
@ -24,7 +24,7 @@
|
||||
"react-toggle": "^4.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "2.8.7"
|
||||
"prettier": "2.8.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@algolia/autocomplete-core": {
|
||||
@ -9918,9 +9918,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "2.8.7",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.7.tgz",
|
||||
"integrity": "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==",
|
||||
"version": "2.8.8",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
|
||||
"integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"prettier": "bin-prettier.js"
|
||||
@ -20601,9 +20601,9 @@
|
||||
"integrity": "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA=="
|
||||
},
|
||||
"prettier": {
|
||||
"version": "2.8.7",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.7.tgz",
|
||||
"integrity": "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==",
|
||||
"version": "2.8.8",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
|
||||
"integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
|
||||
"dev": true
|
||||
},
|
||||
"pretty-error": {
|
||||
|
@ -43,6 +43,6 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "2.8.7"
|
||||
"prettier": "2.8.8"
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ module.exports = {
|
||||
label: "Chat, Communication & Collaboration",
|
||||
items: [
|
||||
"services/bookstack/index",
|
||||
"services/dokuwiki/index",
|
||||
"services/hedgedoc/index",
|
||||
"services/kimai/index",
|
||||
"services/mastodon/index",
|
||||
|