diff --git a/authentik/core/templates/if/admin.html b/authentik/core/templates/if/admin.html index 243772a85..43df9a509 100644 --- a/authentik/core/templates/if/admin.html +++ b/authentik/core/templates/if/admin.html @@ -3,7 +3,7 @@ {% load static %} {% block head %} - + {% endblock %} {% block body %} diff --git a/authentik/core/templates/if/flow.html b/authentik/core/templates/if/flow.html index 0197da38d..3756f4f89 100644 --- a/authentik/core/templates/if/flow.html +++ b/authentik/core/templates/if/flow.html @@ -3,7 +3,7 @@ {% load static %} {% block head %} - + {% endblock %} {% block body %} diff --git a/authentik/flows/tests/test_views.py b/authentik/flows/tests/test_views.py index 7c4f294ff..6664ea28f 100644 --- a/authentik/flows/tests/test_views.py +++ b/authentik/flows/tests/test_views.py @@ -416,7 +416,7 @@ class TestFlowExecutor(TestCase): { "background": flow.background.url, "type": ChallengeTypes.native.value, - "component": "", + "component": "ak-stage-dummy", "title": binding.stage.name, }, ) diff --git a/authentik/root/messages/storage.py b/authentik/root/messages/storage.py index 0999c276c..4e23ca729 100644 --- a/authentik/root/messages/storage.py +++ b/authentik/root/messages/storage.py @@ -25,7 +25,7 @@ class ChannelsStorage(FallbackStorage): uid, { "type": "event.update", - "level_tag": message.level_tag, + "level": message.level_tag, "tags": message.tags, "message": message.message, }, diff --git a/authentik/stages/dummy/stage.py b/authentik/stages/dummy/stage.py index 10f5ad3ca..b4c2f844c 100644 --- a/authentik/stages/dummy/stage.py +++ b/authentik/stages/dummy/stage.py @@ -25,7 +25,7 @@ class DummyStageView(ChallengeStageView): return DummyChallenge( data={ "type": ChallengeTypes.native.value, - "component": "", + "component": "ak-stage-dummy", "title": self.executor.current_stage.name, } ) diff --git a/web/rollup.config.js b/web/rollup.config.js index e86f10842..84045077d 100644 --- a/web/rollup.config.js +++ b/web/rollup.config.js @@ -65,7 +65,7 @@ export default [ }, // Main Application { - input: "./src/main.ts", + input: "./src/interfaces/AdminInterface.ts", output: [ { format: "es", @@ -92,7 +92,7 @@ export default [ }, // Flow executor { - input: "./src/flow.ts", + input: "./src/interfaces/FlowInterface.ts", output: [ { format: "es", diff --git a/web/src/api/Config.ts b/web/src/api/Config.ts index 10a96aa27..71dba42a0 100644 --- a/web/src/api/Config.ts +++ b/web/src/api/Config.ts @@ -4,7 +4,8 @@ import { VERSION } from "../constants"; import { SentryIgnoredError } from "../common/errors"; import { Config, Configuration, RootApi } from "authentik-api"; import { getCookie } from "../utils"; -import { MIDDLEWARE } from "../elements/notifications/APIDrawer"; +import { API_DRAWER_MIDDLEWARE } from "../elements/notifications/APIDrawer"; +import { MessageMiddleware } from "../elements/messages/Middleware"; export const DEFAULT_CONFIG = new Configuration({ basePath: "/api/v2beta", @@ -13,7 +14,8 @@ export const DEFAULT_CONFIG = new Configuration({ "X-Authentik-Prevent-Basic": "true" }, middleware: [ - MIDDLEWARE + API_DRAWER_MIDDLEWARE, + new MessageMiddleware(), ], }); diff --git a/web/src/api/legacy.ts b/web/src/api/legacy.ts index b24dfb43e..76808ada8 100644 --- a/web/src/api/legacy.ts +++ b/web/src/api/legacy.ts @@ -113,4 +113,8 @@ export class FlowURLManager { return `/flows/-/configure/${stageUuid}/${rest}`; } + static cancel(): string { + return "/flows/-/cancel/"; + } + } diff --git a/web/src/elements/buttons/ActionButton.ts b/web/src/elements/buttons/ActionButton.ts index 868c99b36..6e3826f01 100644 --- a/web/src/elements/buttons/ActionButton.ts +++ b/web/src/elements/buttons/ActionButton.ts @@ -2,6 +2,7 @@ import { customElement, property } from "lit-element"; import { ERROR_CLASS, SUCCESS_CLASS } from "../../constants"; import { SpinnerButton } from "./SpinnerButton"; import { showMessage } from "../messages/MessageContainer"; +import { MessageLevel } from "../messages/Message"; @customElement("ak-action-button") export class ActionButton extends SpinnerButton { @@ -26,13 +27,13 @@ export class ActionButton extends SpinnerButton { .catch((e: Error | Response) => { if (e instanceof Error) { showMessage({ - level_tag: "error", + level: MessageLevel.error, message: e.toString() }); } else { e.text().then(t => { showMessage({ - level_tag: "error", + level: MessageLevel.error, message: t }); }); diff --git a/web/src/elements/buttons/ModalButton.ts b/web/src/elements/buttons/ModalButton.ts index 111fc0d04..34061b451 100644 --- a/web/src/elements/buttons/ModalButton.ts +++ b/web/src/elements/buttons/ModalButton.ts @@ -19,6 +19,7 @@ import { convertToSlug } from "../../utils"; import { SpinnerButton } from "./SpinnerButton"; import { PRIMARY_CLASS, EVENT_REFRESH } from "../../constants"; import { showMessage } from "../messages/MessageContainer"; +import { MessageLevel } from "../messages/Message"; @customElement("ak-modal-button") export class ModalButton extends LitElement { @@ -122,7 +123,7 @@ export class ModalButton extends LitElement { }) .catch((e) => { showMessage({ - level_tag: "error", + level: MessageLevel.error, message: "Unexpected error" }); console.error(e); @@ -150,7 +151,7 @@ export class ModalButton extends LitElement { }) .catch((e) => { showMessage({ - level_tag: "error", + level: MessageLevel.error, message: "Unexpected error" }); console.error(e); diff --git a/web/src/elements/forms/ConfirmationForm.ts b/web/src/elements/forms/ConfirmationForm.ts index edc2dbf55..0bfdfd3b2 100644 --- a/web/src/elements/forms/ConfirmationForm.ts +++ b/web/src/elements/forms/ConfirmationForm.ts @@ -2,6 +2,7 @@ import { gettext } from "django"; import { customElement, html, property, TemplateResult } from "lit-element"; import { EVENT_REFRESH } from "../../constants"; import { ModalButton } from "../buttons/ModalButton"; +import { MessageLevel } from "../messages/Message"; import { showMessage } from "../messages/MessageContainer"; @customElement("ak-forms-confirm") @@ -36,14 +37,14 @@ export class ConfirmationForm extends ModalButton { onSuccess(): void { showMessage({ message: gettext(this.successMessage), - level_tag: "success", + level: MessageLevel.success, }); } onError(e: Error): void { showMessage({ message: gettext(`${this.errorMessage}: ${e.toString()}`), - level_tag: "error", + level: MessageLevel.error, }); } diff --git a/web/src/elements/forms/DeleteForm.ts b/web/src/elements/forms/DeleteForm.ts index 7fbce8d8e..45c1e8d09 100644 --- a/web/src/elements/forms/DeleteForm.ts +++ b/web/src/elements/forms/DeleteForm.ts @@ -2,6 +2,7 @@ import { gettext } from "django"; import { customElement, html, property, TemplateResult } from "lit-element"; import { EVENT_REFRESH } from "../../constants"; import { ModalButton } from "../buttons/ModalButton"; +import { MessageLevel } from "../messages/Message"; import { showMessage } from "../messages/MessageContainer"; @customElement("ak-forms-delete") @@ -34,14 +35,14 @@ export class DeleteForm extends ModalButton { onSuccess(): void { showMessage({ message: gettext(`Successfully deleted ${this.objectLabel} ${ this.obj?.name }`), - level_tag: "success", + level: MessageLevel.success, }); } onError(e: Error): void { showMessage({ message: gettext(`Failed to delete ${this.objectLabel}: ${e.toString()}`), - level_tag: "error", + level: MessageLevel.error, }); } diff --git a/web/src/elements/messages/Message.ts b/web/src/elements/messages/Message.ts index 1c215f190..f4e0bf60e 100644 --- a/web/src/elements/messages/Message.ts +++ b/web/src/elements/messages/Message.ts @@ -5,10 +5,17 @@ import PFAlert from "@patternfly/patternfly/components/Alert/alert.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css"; import PFButton from "@patternfly/patternfly/components/Button/button.css"; +export enum MessageLevel { + error = "error", + warning = "warning", + success = "success", + info = "info" +} export interface APIMessage { - level_tag: string; + level: MessageLevel; tags?: string; message: string; + description?: string; } const LEVEL_ICON_MAP: { [key: string]: string } = { @@ -44,13 +51,16 @@ export class Message extends LitElement { render(): TemplateResult { return html`
  • -
    +
    - +

    ${this.message?.message}

    + ${this.message?.description && html`
    +

    ${this.message.description}

    +
    `}
      - ${MIDDLEWARE.requests.map(n => this.renderItem(n))} + ${API_DRAWER_MIDDLEWARE.requests.map(n => this.renderItem(n))}
    diff --git a/web/src/flow.ts b/web/src/flow.ts deleted file mode 100644 index 9fe22476c..000000000 --- a/web/src/flow.ts +++ /dev/null @@ -1,4 +0,0 @@ -import "construct-style-sheets-polyfill"; - -import "./elements/messages/MessageContainer"; -import "./flows/FlowExecutor"; diff --git a/web/src/flows/FlowExecutor.ts b/web/src/flows/FlowExecutor.ts index f8e63a72f..10297e2b4 100644 --- a/web/src/flows/FlowExecutor.ts +++ b/web/src/flows/FlowExecutor.ts @@ -9,6 +9,7 @@ import PFList from "@patternfly/patternfly/components/List/list.css"; import AKGlobal from "../authentik.css"; import { unsafeHTML } from "lit-html/directives/unsafe-html"; +import "./access_denied/FlowAccessDenied"; import "./stages/authenticator_static/AuthenticatorStaticStage"; import "./stages/authenticator_totp/AuthenticatorTOTPStage"; import "./stages/authenticator_validate/AuthenticatorValidateStage"; @@ -16,11 +17,11 @@ import "./stages/authenticator_webauthn/WebAuthnAuthenticatorRegisterStage"; import "./stages/autosubmit/AutosubmitStage"; import "./stages/captcha/CaptchaStage"; import "./stages/consent/ConsentStage"; +import "./stages/dummy/DummyStage"; import "./stages/email/EmailStage"; import "./stages/identification/IdentificationStage"; import "./stages/password/PasswordStage"; import "./stages/prompt/PromptStage"; -import "./access_denied/FlowAccessDenied"; import { ShellChallenge, RedirectChallenge } from "../api/Flows"; import { IdentificationChallenge } from "./stages/identification/IdentificationStage"; import { PasswordChallenge } from "./stages/password/PasswordStage"; @@ -193,6 +194,8 @@ export class FlowExecutor extends LitElement implements StageHost { return html``; case "ak-stage-consent": return html``; + case "ak-stage-dummy": + return html``; case "ak-stage-email": return html``; case "ak-stage-autosubmit": diff --git a/web/src/flows/access_denied/FlowAccessDenied.ts b/web/src/flows/access_denied/FlowAccessDenied.ts index e6fe27c79..bdbd3df16 100644 --- a/web/src/flows/access_denied/FlowAccessDenied.ts +++ b/web/src/flows/access_denied/FlowAccessDenied.ts @@ -14,7 +14,6 @@ import "../../elements/EmptyState"; export interface AccessDeniedChallenge extends Challenge { error_message?: string; - policy_result?: Record; } @customElement("ak-stage-access-denied") @@ -49,27 +48,6 @@ export class FlowAccessDenied extends BaseStage { ${this.challenge?.error_message && html`

    ${this.challenge.error_message}

    `} - ${this.challenge.policy_result && - html`
    - - ${gettext("Explanation:")} - -
      - {% for source_result in policy_result.source_results %} -
    • - {% blocktrans with name=source_result.source_policy.name result=source_result.passing %} - Policy '{{ name }}' returned result '{{ result }}' - {% endblocktrans %} - {% if source_result.messages %} -
        - {% for message in source_result.messages %} -
      • {{ message }}
      • - {% endfor %} -
      - {% endif %} -
    • - {% endfor %} -
    `}
    diff --git a/web/src/flows/stages/authenticator_static/AuthenticatorStaticStage.ts b/web/src/flows/stages/authenticator_static/AuthenticatorStaticStage.ts index e79237d7d..cee973c07 100644 --- a/web/src/flows/stages/authenticator_static/AuthenticatorStaticStage.ts +++ b/web/src/flows/stages/authenticator_static/AuthenticatorStaticStage.ts @@ -12,6 +12,7 @@ import { BaseStage } from "../base"; import "../../../elements/forms/FormElement"; import "../../../elements/EmptyState"; import "../../FormStatic"; +import { FlowURLManager } from "../../../api/legacy"; export const STATIC_TOKEN_STYLE = css` /* Static OTP Tokens */ @@ -61,7 +62,7 @@ export class AuthenticatorStaticStage extends BaseStage { userAvatar="${this.challenge.pending_user_avatar}" user=${this.challenge.pending_user}>
    - ${gettext("Not you?")} + ${gettext("Not you?")}
    - ${gettext("Not you?")} + ${gettext("Not you?")}
    @@ -60,7 +62,7 @@ export class AuthenticatorTOTPStage extends BaseStage { if (!this.challenge?.config_url) return; navigator.clipboard.writeText(this.challenge?.config_url).then(() => { showMessage({ - level_tag: "success", + level: MessageLevel.success, message: gettext("Successfully copied TOTP Config.") }); }); diff --git a/web/src/flows/stages/authenticator_validate/AuthenticatorValidateStage.ts b/web/src/flows/stages/authenticator_validate/AuthenticatorValidateStage.ts index 21c49bbf2..e048cf178 100644 --- a/web/src/flows/stages/authenticator_validate/AuthenticatorValidateStage.ts +++ b/web/src/flows/stages/authenticator_validate/AuthenticatorValidateStage.ts @@ -11,6 +11,7 @@ import AKGlobal from "../../../authentik.css"; import { BaseStage, StageHost } from "../base"; import "./AuthenticatorValidateStageWebAuthn"; import "./AuthenticatorValidateStageCode"; +import { PasswordManagerPrefill } from "../identification/IdentificationStage"; export enum DeviceClasses { STATIC = "static", @@ -83,6 +84,17 @@ export class AuthenticatorValidateStage extends BaseStage implements StageHost { ${gettext("Use a security key to prove your identity.")} `; case DeviceClasses.TOTP: + // TOTP is a bit special, assuming that TOTP is allowed from the backend, + // and we have a pre-filled value from the password manager, + // directly set the the TOTP device Challenge as active. + if (PasswordManagerPrefill.totp) { + console.debug("authentik/stages/authenticator_validate: found prefill totp code, selecting totp challenge"); + this.selectedDeviceChallenge = deviceChallenge; + // Delay the update as a re-render isn't triggered from here + setTimeout(() => { + this.requestUpdate(); + }, 100); + } return html`

    ${gettext("Traditional authenticator")}

    @@ -141,9 +153,9 @@ export class AuthenticatorValidateStage extends BaseStage implements StageHost { render(): TemplateResult { if (!this.challenge) { return html` -`; + ?loading="${true}" + header=${gettext("Loading")}> + `; } // User only has a single device class, so we don't show a picker if (this.challenge?.device_challenges.length === 1) { diff --git a/web/src/flows/stages/authenticator_validate/AuthenticatorValidateStageCode.ts b/web/src/flows/stages/authenticator_validate/AuthenticatorValidateStageCode.ts index 0a094ce4f..d5be69405 100644 --- a/web/src/flows/stages/authenticator_validate/AuthenticatorValidateStageCode.ts +++ b/web/src/flows/stages/authenticator_validate/AuthenticatorValidateStageCode.ts @@ -13,6 +13,7 @@ import "../../../elements/forms/FormElement"; import "../../../elements/EmptyState"; import { PasswordManagerPrefill } from "../identification/IdentificationStage"; import "../../FormStatic"; +import { FlowURLManager } from "../../../api/legacy"; @customElement("ak-stage-authenticator-validate-code") export class AuthenticatorValidateStageWebCode extends BaseStage { @@ -44,7 +45,7 @@ export class AuthenticatorValidateStageWebCode extends BaseStage { userAvatar="${this.challenge.pending_user_avatar}" user=${this.challenge.pending_user}>
    - ${gettext("Not you?")} + ${gettext("Not you?")}
    - ${gettext("Not you?")} + ${gettext("Not you?")}
    diff --git a/web/src/flows/stages/consent/ConsentStage.ts b/web/src/flows/stages/consent/ConsentStage.ts index 9e0d3a93d..f8c1f1d8e 100644 --- a/web/src/flows/stages/consent/ConsentStage.ts +++ b/web/src/flows/stages/consent/ConsentStage.ts @@ -11,6 +11,7 @@ import AKGlobal from "../../../authentik.css"; import { BaseStage } from "../base"; import "../../../elements/EmptyState"; import "../../FormStatic"; +import { FlowURLManager } from "../../../api/legacy"; export interface Permission { name: string; @@ -53,7 +54,7 @@ export class ConsentStage extends BaseStage { userAvatar="${this.challenge.pending_user_avatar}" user=${this.challenge.pending_user}>
    diff --git a/web/src/flows/stages/dummy/DummyStage.ts b/web/src/flows/stages/dummy/DummyStage.ts new file mode 100644 index 000000000..d54d3389c --- /dev/null +++ b/web/src/flows/stages/dummy/DummyStage.ts @@ -0,0 +1,52 @@ +import { gettext } from "django"; +import { CSSResult, customElement, html, property, TemplateResult } from "lit-element"; +import { Challenge } from "../../../api/Flows"; +import PFLogin from "@patternfly/patternfly/components/Login/login.css"; +import PFForm from "@patternfly/patternfly/components/Form/form.css"; +import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css"; +import PFTitle from "@patternfly/patternfly/components/Title/title.css"; +import PFButton from "@patternfly/patternfly/components/Button/button.css"; +import PFBase from "@patternfly/patternfly/patternfly-base.css"; +import AKGlobal from "../../../authentik.css"; +import { BaseStage } from "../base"; +import "../../../elements/EmptyState"; +import "../../FormStatic"; + +@customElement("ak-stage-dummy") +export class DummyStage extends BaseStage { + + @property({ attribute: false }) + challenge?: Challenge; + + static get styles(): CSSResult[] { + return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal]; + } + + render(): TemplateResult { + if (!this.challenge) { + return html` + `; + } + return html` + +
    + +
    `; + } + +} diff --git a/web/src/flows/stages/password/PasswordStage.ts b/web/src/flows/stages/password/PasswordStage.ts index 868e84193..0cab171e8 100644 --- a/web/src/flows/stages/password/PasswordStage.ts +++ b/web/src/flows/stages/password/PasswordStage.ts @@ -13,6 +13,7 @@ import "../../../elements/forms/FormElement"; import "../../../elements/EmptyState"; import { PasswordManagerPrefill } from "../identification/IdentificationStage"; import "../../FormStatic"; +import { FlowURLManager } from "../../../api/legacy"; export interface PasswordChallenge extends WithUserInfoChallenge { recovery_url?: string; @@ -47,7 +48,7 @@ export class PasswordStage extends BaseStage { userAvatar="${this.challenge.pending_user_avatar}" user=${this.challenge.pending_user}> diff --git a/web/src/interfaces/AdminInterface.ts b/web/src/interfaces/AdminInterface.ts index a31b5b48a..0418c459e 100644 --- a/web/src/interfaces/AdminInterface.ts +++ b/web/src/interfaces/AdminInterface.ts @@ -1,3 +1,9 @@ +import "construct-style-sheets-polyfill"; + +// Elements that are used by SiteShell pages +// And can't dynamically be imported +import "../elements/CodeMirror"; +import "../elements/messages/MessageContainer"; import { customElement } from "lit-element"; import { me } from "../api/Users"; import { SidebarItem } from "../elements/sidebar/Sidebar"; diff --git a/web/src/interfaces/FlowInterface.ts b/web/src/interfaces/FlowInterface.ts new file mode 100644 index 000000000..9ba725a8c --- /dev/null +++ b/web/src/interfaces/FlowInterface.ts @@ -0,0 +1,4 @@ +import "construct-style-sheets-polyfill"; + +import "../elements/messages/MessageContainer"; +import "../flows/FlowExecutor"; diff --git a/web/src/interfaces/admin/index.html b/web/src/interfaces/admin/index.html index 793861dac..dcaad682b 100644 --- a/web/src/interfaces/admin/index.html +++ b/web/src/interfaces/admin/index.html @@ -8,7 +8,8 @@ - + + authentik diff --git a/web/src/interfaces/flow/index.html b/web/src/interfaces/flow/index.html index 9d3071349..ceec0869c 100644 --- a/web/src/interfaces/flow/index.html +++ b/web/src/interfaces/flow/index.html @@ -8,7 +8,8 @@ - + + authentik diff --git a/web/src/main.ts b/web/src/main.ts deleted file mode 100644 index 12997f4e3..000000000 --- a/web/src/main.ts +++ /dev/null @@ -1,13 +0,0 @@ -import "construct-style-sheets-polyfill"; - -// Elements that are used by SiteShell pages -// And can't dynamically be imported -import "./elements/buttons/ActionButton"; -import "./elements/buttons/Dropdown"; -import "./elements/buttons/ModalButton"; -import "./elements/buttons/SpinnerButton"; -import "./elements/CodeMirror"; - -import "./pages/generic/SiteShell"; -import "./interfaces/AdminInterface"; -import "./elements/messages/MessageContainer"; diff --git a/web/src/pages/flows/FlowListPage.ts b/web/src/pages/flows/FlowListPage.ts index 84c7d9d76..1dc88d311 100644 --- a/web/src/pages/flows/FlowListPage.ts +++ b/web/src/pages/flows/FlowListPage.ts @@ -57,8 +57,8 @@ export class FlowListPage extends TablePage { `, html`${item.name}`, html`${item.designation}`, - html`${item.stages?.size}`, - html`${item.policies?.size}`, + html`${Array.from(item.stages || []).length}`, + html`${Array.from(item.policies || []).length}`, html` diff --git a/web/src/pages/generic/SiteShell.ts b/web/src/pages/generic/SiteShell.ts index 5d87c83a9..1ec767f7e 100644 --- a/web/src/pages/generic/SiteShell.ts +++ b/web/src/pages/generic/SiteShell.ts @@ -19,6 +19,7 @@ import AKGlobal from "../../authentik.css"; import CodeMirrorStyle from "codemirror/lib/codemirror.css"; import CodeMirrorTheme from "codemirror/theme/monokai.css"; import { EVENT_REFRESH } from "../../constants"; +import { MessageLevel } from "../../elements/messages/Message"; @customElement("ak-site-shell") export class SiteShell extends LitElement { @@ -79,7 +80,7 @@ export class SiteShell extends LitElement { } console.debug(`authentik/site-shell: Request failed ${this._url}`); showMessage({ - level_tag: "error", + level: MessageLevel.error, message: gettext(`Request failed: ${response.statusText}`), }); this.loading = false; @@ -148,7 +149,7 @@ export class SiteShell extends LitElement { }) .catch((e) => { showMessage({ - level_tag: "error", + level: MessageLevel.error, message: "Unexpected error" }); console.error(e); diff --git a/web/src/pages/users/UserActiveForm.ts b/web/src/pages/users/UserActiveForm.ts index ed7aa7b4d..ed1868521 100644 --- a/web/src/pages/users/UserActiveForm.ts +++ b/web/src/pages/users/UserActiveForm.ts @@ -1,6 +1,7 @@ import { gettext } from "django"; import { customElement, html, TemplateResult } from "lit-element"; import { DeleteForm } from "../../elements/forms/DeleteForm"; +import { MessageLevel } from "../../elements/messages/Message"; import { showMessage } from "../../elements/messages/MessageContainer"; @customElement("ak-user-active-form") @@ -9,14 +10,14 @@ export class UserActiveForm extends DeleteForm { onSuccess(): void { showMessage({ message: gettext(`Successfully updated ${this.objectLabel} ${this.obj?.name}`), - level_tag: "success", + level: MessageLevel.success, }); } onError(e: Error): void { showMessage({ message: gettext(`Failed to update ${this.objectLabel}: ${e.toString()}`), - level_tag: "error", + level: MessageLevel.error, }); }