diff --git a/authentik/core/api/applications.py b/authentik/core/api/applications.py index 413e7777a9..eddad64bf8 100644 --- a/authentik/core/api/applications.py +++ b/authentik/core/api/applications.py @@ -22,6 +22,7 @@ from rest_framework.viewsets import ModelViewSet from structlog.stdlib import get_logger from authentik.admin.api.metrics import CoordinateSerializer +from authentik.api.pagination import Pagination from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT from authentik.core.api.providers import ProviderSerializer from authentik.core.api.used_by import UsedByMixin @@ -43,9 +44,12 @@ from authentik.rbac.filters import ObjectFilter LOGGER = get_logger() -def user_app_cache_key(user_pk: str) -> str: +def user_app_cache_key(user_pk: str, page_number: int | None = None) -> str: """Cache key where application list for user is saved""" - return f"{CACHE_PREFIX}/app_access/{user_pk}" + key = f"{CACHE_PREFIX}/app_access/{user_pk}" + if page_number: + key += f"/{page_number}" + return key class ApplicationSerializer(ModelSerializer): @@ -213,7 +217,8 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet): return super().list(request) queryset = self._filter_queryset_for_list(self.get_queryset()) - paginated_apps = self.paginate_queryset(queryset) + paginator: Pagination = self.paginator + paginated_apps = paginator.paginate_queryset(queryset, request) if "for_user" in request.query_params: try: @@ -235,12 +240,14 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet): if not should_cache: allowed_applications = self._get_allowed_applications(paginated_apps) if should_cache: - allowed_applications = cache.get(user_app_cache_key(self.request.user.pk)) + allowed_applications = cache.get( + user_app_cache_key(self.request.user.pk, paginator.page.number) + ) if not allowed_applications: - LOGGER.debug("Caching allowed application list") + LOGGER.debug("Caching allowed application list", page=paginator.page.number) allowed_applications = self._get_allowed_applications(paginated_apps) cache.set( - user_app_cache_key(self.request.user.pk), + user_app_cache_key(self.request.user.pk, paginator.page.number), allowed_applications, timeout=86400, ) diff --git a/web/src/user/LibraryPage/LibraryPage.ts b/web/src/user/LibraryPage/LibraryPage.ts index cdd54f8f72..01ab26c718 100644 --- a/web/src/user/LibraryPage/LibraryPage.ts +++ b/web/src/user/LibraryPage/LibraryPage.ts @@ -2,7 +2,6 @@ import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { me } from "@goauthentik/common/users"; import { AKElement, rootInterface } from "@goauthentik/elements/Base"; import "@goauthentik/elements/EmptyState"; -import { PaginatedResponse } from "@goauthentik/elements/table/Table"; import { localized, msg } from "@lit/localize"; import { html } from "lit"; @@ -25,6 +24,8 @@ import type { PageUIConfig } from "./types"; * */ +const coreApi = () => new CoreApi(DEFAULT_CONFIG); + @localized() @customElement("ak-library") export class LibraryPage extends AKElement { @@ -35,15 +36,13 @@ export class LibraryPage extends AKElement { isAdmin = false; @state() - apps!: PaginatedResponse; + apps: Application[] = []; @state() uiConfig: PageUIConfig; constructor() { super(); - const applicationListFetch = new CoreApi(DEFAULT_CONFIG).coreApplicationsList({}); - const meFetch = me(); const uiConfig = rootInterface()?.uiConfig; if (!uiConfig) { throw new Error("Could not retrieve uiConfig. Reason: unknown. Check logs."); @@ -55,22 +54,41 @@ export class LibraryPage extends AKElement { searchEnabled: uiConfig.enabledFeatures.search, }; - Promise.allSettled([applicationListFetch, meFetch]).then( - ([applicationListStatus, meStatus]) => { - if (meStatus.status === "rejected") { - throw new Error( - `Could not determine status of user. Reason: ${meStatus.reason}`, - ); + Promise.all([this.fetchApplications(), me()]).then(([applications, meStatus]) => { + this.isAdmin = meStatus.user.isSuperuser; + this.apps = applications; + this.ready = true; + }); + } + + async fetchApplications(): Promise { + const applicationListParams = (page = 1) => ({ + ordering: "name", + page, + pageSize: 100, + }); + + const applicationListFetch = await coreApi().coreApplicationsList(applicationListParams(1)); + const pageCount = applicationListFetch.pagination.totalPages; + if (pageCount === 1) { + return applicationListFetch.results; + } + + const applicationLaterPages = await Promise.allSettled( + Array.from({ length: pageCount - 1 }).map((_a, idx) => + coreApi().coreApplicationsList(applicationListParams(idx + 2)), + ), + ); + + return applicationLaterPages.reduce( + function (acc, result) { + if (result.status === "rejected") { + const reason = JSON.stringify(result.reason, null, 2); + throw new Error(`Could not retrieve list of applications. Reason: ${reason}`); } - if (applicationListStatus.status === "rejected") { - throw new Error( - `Could not retrieve list of applications. Reason: ${applicationListStatus.reason}`, - ); - } - this.isAdmin = meStatus.value.user.isSuperuser; - this.apps = applicationListStatus.value; - this.ready = true; + return [...acc, ...result.value.results]; }, + [...applicationListFetch.results], ); } diff --git a/web/src/user/LibraryPage/LibraryPageImpl.ts b/web/src/user/LibraryPage/LibraryPageImpl.ts index 76d82793c1..1892e499f9 100644 --- a/web/src/user/LibraryPage/LibraryPageImpl.ts +++ b/web/src/user/LibraryPage/LibraryPageImpl.ts @@ -1,7 +1,6 @@ import { groupBy } from "@goauthentik/common/utils"; import { AKElement } from "@goauthentik/elements/Base"; import "@goauthentik/elements/EmptyState"; -import { PaginatedResponse } from "@goauthentik/elements/table/Table"; import "@goauthentik/user/LibraryApplication"; import { msg } from "@lit/localize"; @@ -42,8 +41,8 @@ export class LibraryPage extends AKElement { @property({ attribute: "isadmin", type: Boolean }) isAdmin = false; - @property({ attribute: false }) - apps!: PaginatedResponse; + @property({ attribute: false, type: Array }) + apps!: Application[]; @property({ attribute: false }) uiConfig!: PageUIConfig; @@ -66,7 +65,7 @@ export class LibraryPage extends AKElement { connectedCallback() { super.connectedCallback(); - this.filteredApps = this.apps?.results; + this.filteredApps = this.apps; if (this.filteredApps === undefined) { throw new Error( "Application.results should never be undefined when passed to the Library Page.", @@ -89,7 +88,7 @@ export class LibraryPage extends AKElement { event.stopPropagation(); const apps = event.detail.apps; this.selectedApp = undefined; - this.filteredApps = this.apps.results; + this.filteredApps = this.apps; if (apps.length > 0) { this.selectedApp = apps[0]; this.filteredApps = event.detail.apps; @@ -132,7 +131,7 @@ export class LibraryPage extends AKElement { } renderSearch() { - return html``; + return html``; } render() {