web: Application wizard v2 with tests (#7004)
* A lot of comments about forms. * Adding comments to the wizard. * Broke out the text input into a single renderer. Still works as required. * web: Legibility in the ApplicationForm. This is a pretty good result. By using the LightDOM setting, this provides the existing Authentik form manager with access to the ak-form-horizontal-element components without having to do any cross-border magic. It's not ideal, and it shows up just how badly we've got patternfly splattered everywhere, but the actual results are remarkable. The patterns for text, switch, radio, textarea, file, and even select are smaller and easier here. I'm still noodling on what an unspread search-select element would look like. It's just dependency injection, so it ought to be as straightforward as that. * web: Marking down the start of the 'components' library. * web: Baby steps I become frustrated with my inability to make any progress on this project, so I decided to reach for a tool that I consider highly reliable but also incredibly time-consuming and boring: test driven development. In this case, I wrote a story about how I wanted to see the first page rendered: just put the HTML tag, completely unadorned, that will handle the first page of the wizard. Then, add an event handler that will send the updated content to some parent object, since what we really want is to orchestrate the state of the user's input with a centralized location. Then, rather than fiddling with the attributes and properties of the various pages, I wanted them to be able to "look up" the values they want, much as we'd expect a standalone form to be able to pull its values from the server, so I added a context object that receives the update event and incorporates the new knowledge about the state of the process into itself. The result is surprisingly satisfying: the first page renders cleanly, displays the content that we want, and as we fiddle with, we can *watch in real time* as the results of the context are updated and retransmitted to all receiving objects. And the sending object gets the results so it re-renders, but it ends up looking the same as it was before the render. * Now, it's starting to look like a complete package. The LDAP method is working, but there is a bug: the radio is sending the wrong value !?!?!?. Track that down, dammit. The search wrappers now resend their events as standard `input` events, and that actually seems to work well; the browser is decorating it with the right target, with the right `name` attribute, and since we have good definitions of the `value` as a string (the real value of any search object is its UUID4), that works quite well. Added search wrappers for CoreGroup and CryptoCertificate (CertificateKeyPairs), and the latter has flags for "use the first one if it's the only one" and "allow the display of keyless certificates." Not sure why `state()` is blocking the transmission of typing information from the typed element to the context handler, but it's a bug in the typechecker, and it's not a problem so far. * Now, it's starting to look like a complete package. The LDAP method is working, but there is a bug: the radio is sending the wrong value !?!?!?. Track that down, dammit. The search wrappers now resend their events as standard `input` events, and that actually seems to work well; the browser is decorating it with the right target, with the right `name` attribute, and since we have good definitions of the `value` as a string (the real value of any search object is its UUID4), that works quite well. Added search wrappers for CoreGroup and CryptoCertificate (CertificateKeyPairs), and the latter has flags for "use the first one if it's the only one" and "allow the display of keyless certificates." Not sure why `state()` is blocking the transmission of typing information from the typed element to the context handler, but it's a bug in the typechecker, and it's not a problem so far. * web: tracked down that weirld bug with the radio. Because radio inputs are actually multiples, the events handling for radio is... wonky. If we want our `<ak-radio>` component to be a unitary event dispatcher, saying "This is the element selected," we needed to do more than what was currently being handled. I've intercepted the events that we care about and have placed them into a controller that dictates both the setting and the re-render of the component. This makes it "controlled" (to use the Angular/React/Vue) language and depends on Lit's reactiveElement lifecycle to work, rather than trust the browser, but the browser's experience with respect to the `<input type=radio` is pretty bad: both input elements fire events, one for "losing selection" and one for "gaining selection". That can be very confusing to handle, so we funnel them down in our aggregate radio element to a single event, "selection changed". As a quality-of-life measure, I've also set the label to be unselectable; this means that a click on the label will trigger the selection event, and a long click will not disable selection or confuse the selection event generator. * web: now passing the precommit phase * web: a HACK for Storybook to inject the "use light theme" flag into the body. This isn't really a very good hack; what it does is say that every story is responsible for hacking its theme into the parent. This is very annoying, but it does mean that we can at least show our components in the best light. * web: ak-application-wizard-authentication-by-oauth, and many fixes! 1. Fixed `eventEmitter` so that if the detail object is a scalar, it will not attempt to "objectify" it. This was causing a bug where retrofitting the eventEmitter to some older components resulted in a detail of "some" being translated into ['s', 'o', 'm', 'e']. Not what is wanted. 2. Removed the "transitional form" from the existing components; they had a two-step where the web component class was just a wrapper around an independent rendering function. While this worked, it was only to make the case that they *were* independent rendering objects and could be supported with the right web component framework. We're halfway there now; the last step will be to transform the horizontal-element and various input CSS into componentized CSS, the way Patternfly-Elements is currently doing. 3. Fixed the `help` field so that it could take a string or a TemplateResult, and if the latter, don't bother wrapping it in the helper text functionality; just let it be its own thing. This supports the multi-line help of redirectURI as well as the `ak-utils-time-delta` capability. 4. Transform Oauth2ProviderForm to use the new components, to the best of our ability. Also used the `provider = this.wizard.provider` and `provider = this.instance` syntax to make the render function *completely portable*; it's the exact same text that is dropped into... 5. The complete `ak-application-wizard-authentication-by-oauth` component. They're so similar part of me wonders if I could push them both out to a common reference, or a collection of common references. Both components use the PropertyMapping and Sources, and both use the same collection of searches (Crypto, Flow). 6. A Storybook for `ak-application-wizard-authentication-by-oauth`, showing the works working. 7. New mocks for `authorizationFlow`, `propertyMappings`, and `hasJWKs`. This sequence has revealed a bug in the radio control. (It's always the radio control.) If the default doesn't match the current setting, the radio control doesn't behave as expected; it won't change when you fully expect that it should. I'll investigate how to harmonize those tomorrow. * web: Converted our toggle groups to a more streamlined implementation. * web: one more toggle group. * initial api and schema Signed-off-by: Jens Langhammer <jens@goauthentik.io> * separate blueprint importer from yaml parsing Signed-off-by: Jens Langhammer <jens@goauthentik.io> * cleanup Signed-off-by: Jens Langhammer <jens@goauthentik.io> * web: Replace ad-hoc toggle control with ak-toggle-group This commit replaces various ad-hoc implementations of the Patternfly Toggle Group HTML with a web component that encapsulates all of the needed behavior and exposes a single API with a single event handler, return the value of the option clicked. The results are: Lots of visual clutter is eliminated. A single link of: ``` <div class="pf-c-toggle-group__item"> <button class="pf-c-toggle-group__button ${this.mode === ProxyMode.Proxy ? "pf-m-selected" : ""}" type="button" @click=${() => { this.mode = ProxyMode.Proxy; }}> <span class="pf-c-toggle-group__text">${msg("Proxy")}</span> </button> </div> <div class="pf-c-divider pf-m-vertical" role="separator"></div> ``` Now looks like: ``` <option value=${ProxyMode.Proxy}>${msg("Proxy")}</option> ``` This also means that the three pages that used the Patternfly Toggle Group could eliminate all of their Patternfly PFToggleGroup needs, as well as the `justify-content: center` extension, which also eliminated the `css` import. The savings aren't as spectacular as I'd hoped: removed 178 lines, but added 123; total savings 55 lines of code. I still count this a win: we need never write another toggle component again, and any bugs, extensions or features we may want to add can be centralized or forked without risking the whole edifice. * web: minor code formatting issue. * add new "must_created" state to blueprints to prevent overwriting objects Signed-off-by: Jens Langhammer <jens@goauthentik.io> * web: adding a storybook for the ak-toggle-group component * Bugs found by CI/CD. * web: Replace ad-hoc search for CryptoCertificateKeyPairs with ak-crypto-certeficate-search This commit replaces various ad-hoc implementations of `search-select` for CryptoCertificateKeyPairs with a web component that encapsulates all of the needed behavior and exposes a single API. The results are: Lots of visual clutter is eliminated. A single search of: ```HTML <ak-search-select .fetchObjects=${async (query?: string): Promise<CertificateKeyPair[]> => { const args: CryptoCertificatekeypairsListRequest = { ordering: "name", hasKey: true, includeDetails: false, }; if (query !== undefined) { args.search = query; } const certificates = await new CryptoApi( DEFAULT_CONFIG, ).cryptoCertificatekeypairsList(args); return certificates.results; }} .renderElement=${(item: CertificateKeyPair): string => { return item.name; }} .value=${(item: CertificateKeyPair | undefined): string | undefined => { return item?.pk; }} .selected=${(item: CertificateKeyPair): boolean => { return this.instance?.tlsVerification === item.pk; }} ?blankable=${true} > </ak-search-select> ``` Now looks like: ```HTML <ak-crypto-certificate-search certificate=${this.instance?.tlsVerification}> </ak-crypto-certificate-search> ``` There are three searches that do not require there to be a valid key with the certificate; these are supported with the boolean property `nokey`; likewise, there is one search (in SAMLProviderForm) that states that if there is no current certificate in the SAMLProvider and only one certificate can be found in the Authentik database, use that one; this is supported with the boolean property `singleton`. These changes replace 382 lines of object-oriented invocations with 36 lines of declarative configuration, and 98 lines for the class. Overall, the code for "find a crypto certificate" has been reduced by 46%. Suggestions for a better word than `singleton` are welcome! * web: display tests for CryptoCertificateKeypair search This adds a Storybook for the CryptoCertificateKeypair search, including a mock fetch of the data. In the course of running the tests, we discovered that including the SearchSelect _class_ won't include the customElement declaration unless you include the whole file! Other bugs found: including the CSS from Storybook is different from that of LitElement native, so much so that the adapter needed to be included. FlowSearch had a similar bug. The problem only manifests when building via Webpack (which Storybook uses) and not Rollup, but we should support both in distribution. * Fixed behavioral problem with the radio; the `if` there was preventing the radio from reflecting the default correctly. The observed behavior was that the radio wouldn't "activate" until the item selected during the render pass was clicked on first. * Proxy Provider done. * web: Tactical change. Put all the variants on the second page; it's a longer list, but it's also easier to manage than all those required sub-options. * Rounding out the catalog. * web: SAML Manual Configuration Added a 'design document' that just kinda describes what I'm trying to do, in case I don't get this done by Friday Aug 11, 2023. I had two tables doing the same thing, so I merged them and then wrote a few map/filters to specialize them for those two use cases. Along the way I had to fiddle with the ESLint settings so that underscore-prefixed unused variables would be ignored. I cleaned up the visual appeal of the forms in the LDAP application. I was copy/pasting the "handleProviderEvent" function, so I pulled it out into ApplicationWizardProviderPageBase. Not so much a matter of abstraction as just disliking that kind of duplication; it served no purpose. * Added SAML Story to Storybook. * Web: This is coming together amazingly well. Like, almost too well. * web: 80% of the way there This commit includes the first three pages of the wizard, the completion of the wizard framework with evented handling, and control over progression. Some shortcomings of this design have become evident: it isn't possible to communicate between the steps' wrappers, as they are POJOs without access to the context. An imperative decision-making process has to be inserted in the orchestration layer, which is kinda annoying. But it looks good and it behaves correctly, to the extent that I've given it behavior. It's an excellent foundation. * Linting. * web: application wizard Found where the hook for form validity should go. Excellent! Now I just need to incorporate that basic validation into the business logic and we're good to go. * Turns out that was one layer too many; the topmost component was fine for maintaining the context. * It looks like my brilliant strategy has hit a snag. The idea is simple. Let's start with this picture: ``` <application-wizard .steps=${[... a collection of step objects ...]}> <wizard-main .steps=${(steps from above)}> <application-current-panel> <current-form> ``` - ApplicationWizard has a Context for the ApplicationProviderPair (or whatever it's going to be). This context does not know about the steps; it just knows about: the "application" object, the "provider" object, and a discriminator to know *which* provider the user has selected. - ApplicationWizard has Steps that, among other things, provides Panels for: - Application - Pick Provider - Configure Provider - Submit ApplicationProviderPair to the back-end - The WizardFrame renders the CurrentPanel for the CurrentStep The CurrentPanel gets its data from the ApplicationWizard in the form of a Context. It then sends messages (events) to ApplicationWizard about the contents of each field as the user is filling out the form, so that the ApplicationWizard can record those in the ApplicationProviderPair for later submission. When a CurrentForm is valid, the ApplicationWizard updates the Steps object to show that the "Next button" on the Wizard is now available. In this way, the user can progress through the system. When they get to the last page, we can provide in the ApplicationWizard with the means to submit the form and/or send the user back to the page with the validation failure. Problem: The context is being updated in real-time, which is triggering re-renders of the form. This leads to focus problems as the fields that are not yet valid are triggering "focus grab" behavior. This is a classic problem with "controlled" inputs. What we really want is for the CurrentPanel to not re-render at all, but to behave like a normal, uncontrolled form, and let the browser do most of the work. We still want the [Next] button to enable when the form is valid enough to permit that. --- Other details: I've ripped out a lot of Jen's work, which is probably a mistake. It's still preserved elsewhere. I've also cleaned up the various wizardly things to try and look organized. It *looks* like it should work, it just... doesn't. Not yet. * Late addition: I had an inspiration about how to reduce the way reactivity broke focus by, basically, removing the reactivity and managing the first-time-through lifecycle to prevent the update from causing refocus. It works well! Now I just need to test it. * This application fixes the bug with respect to the wizard-level context being updated incorrectly. Understandings: - To use uncontrolled inputs, which I prefer, the context object should not be a state or property at the level of consumers; it should not automatically re-render with every keystroke, i.e. "The React Way." We're using Web Components, [client-side validation](https://developer.mozilla.org/en-US/docs/Learn/Forms/Form_validation) exists on the platform already, and live-validation is problematic for any number of reasons. - The trade-off is that it is now necessary to re-render the target page of the wizard de-novo, but that's not really as big a deal as it sounds. Lit is ready to do that... and then nothing else until we request a change-of-page. Excellent. - The top level context *must* be a state, but it's better if it's a state never actually used by the top-level context container. The debate about whether or not to make that container a dumb one (`<slot></slot>`) or to merge it with the top-level object continues; here, I've merged it with the top-level wizard object, but that object does not refer to the state variable being managed in its render pass, so changes to it do not cause a re-render of the whole wizard. The purpose of the top-level page is to manage the *steps*, not the *content of any step*. A step may change dynamically based on the content of a step, but that's the same thing as *which step*. Lesson: always know what your state is *about*. - Deep merging is a complex subject, but here it's appropriate to our needs. * web: Application Wizard This commit combines a working (but very unpolished) version of the Application Wizard with Jen's code for the CoreTransactionApplicationRequest, resulting in a successful round trip. It fixes a number of bugs with the way ContextProducer decorators were being processed, such that they just weren't working with our current configuration (although they did work fine in Storybook); consumers didn't need to be fixed. It also *removes* the steps-aware context from the Wizard. That *may* be a mistake. To re-iterate, the `WizardFrame` provides the chrome for a Wizard: the button bar div, the breadcrumbs div, the header div, and it takes the steps object as its source of truth for all of the content. The `WizardContent` part of the application has two parts: The `WizardMain`, which wraps the frame and supplies the context for all the `WizardPanels`, and the `WizardPanels` themselves, which are dependent on a context from `WizardMain` for the data that populates each panel. YAGNI right now that the panels need to know anything about the steps, and the `WizardMain` can just pass a fresh `.steps` object to the `WizardFrame` when they need updating. Using props drilling may make more sense here. It certainy does *not* make sense for the panels. They need to be renderable on-demand, and they need to make sense of what they're rendering on-demand, so the function is ``` (panel code) => (context) => (rendered panel) ``` (Yes, that's curried notation. Deal.) * This commit includes the first WDIO test for the ApplicationWizard. It doesn't do much right now, but it does log in and navigate to the wizard successfully. * web: completed test for single application, provided new programming language to make it easier to write tests. * Almost there. Missing: The validation is currently not working as expected, and I cannot get the backend to give me meaningful data helping us "go back" to the field that wasn't valid. I really don't want to put all the meaningful validation on the front-end; that's the road to - perdition, the back-end must be usable by people less assiduous than we are. Also: Need to make the button bar work better; maybe each panel can provide a custom button bar if one is needed? * web: Test harness We have an end-to-end test harness that includes a trivially correct DSL for "This is what a user would do, do this": ``` const deleteProvider = (theSlug) => ([ ["button", '>>>ak-sidebar-item a[href="#/core/providers"]'], ["deletebox", `>>>a[href="#/core/applications/${theSlug}"]`], ["button", '>>>ak-forms-delete-bulk button[slot="trigger"]'], ["button", '>>>ak-forms-delete-bulk div[role="dialog"] ak-spinner-button'], ]); ``` It's now possible to target individual sequences of events this way. With a little creativity, we could have standalone functions that take parameters for our calls and just do them, without too much struggle. * web: Revised navigation After working with the navigation for awhile, I realized that it's a poor map; what I really wanted was a controller/view pair, where events flow up to the controller and then messages on "what to draw" flow down to the view. It work quite well, and the wizard frame is smaller and smarter for it. I've also moved the WDIO-driven tests into the 'tests' folder, because it (a) makes more sense to put them there, and (b) it prevents any confusion about who's in charge of node_modules. * web: Simplify, simplify, simplify Sort-of. This commit changes the way the "wizard step coordinator" layer works, giving the wizard writer much more power over button bar. It still assumes there are only three actions the wizard frame wants to commit: next, back, and close. This empowers the steps themselves to re-arrange their buttons and describe the rules through which transitions occur. * web: resetting the form is not working yet... I vehemently dislike the object-oriented "reset" command; every wizard should start with an absolutely fresh copy of the data upon entry. Refactoring the wizard to re-build its content from the inside is the correct way to go, but I don't have a good mental image of how to make the ModalButton and the component it invokes interact cleanly, which frustrates the hell out of me. * web: reset As I said, I greatly dislike having to be dependent upon "resets"; I prefer my data to be de novo going into a "new" transaction. That said, we work with what we've got; I've created an event generated by the wizard that says the modal just closed; anything wrapping and implementing the wizard can then capture that event and reset the data. I've also added a pair of functions that create the two states (what step, what form data) anew, so that resetting is as trivial as initializing (and is exactly the same, code-wise). * web: Without error handling, this is complete, but I still need @BeryJu (Jens) for help with the SAML Upload (it doesn't appear to be correctly handled?) and the error handling. * web: revise tests for wizard This commit replaces the previous WDIO instance with a more formal and straightforward process using the [pageobjects](https://martinfowler.com/bliki/PageObject.html). In this form, every major component has its own test suite, and a test is a sequence of exercises of those components. A test then becomes something as straightforward as: ``` await LoginPage.open(); await LoginPage.login("ken@goauthentik.io", "eat10bugs"); expect(await UserLibraryPage.pageHeader).toHaveText("My Applications"); await UserLibraryPage.goToAdmin(); expect(await AdminOverviewPage.pageHeader).toHaveText("Welcome, "); await AdminOverviewPage.openApplicationsListPage(); expect(await ApplicationsListPage.pageHeader).toHaveText("Applications"); ApplicationsListPage.startCreateApplicationWizard(); await ApplicationWizard.app.name.setValue(`Test application ${newId}`); await ApplicationWizard.nextButton.click(); await (await ApplicationWizard.getProviderType("ldapprovider")).click(); await ApplicationWizard.nextButton.click(); await ApplicationWizard.ldap.setBindFlow("default-authentication-flow"); await ApplicationWizard.nextButton.click(); await expect(await ApplicationWizard.commitMessage).toHaveText( "Your application has been saved" ); ``` Whether or not there's another layer of DSL in there or not, this is a pretty nice idiom for maintaining tests. * web: updating with forms and fixes for eslint complaints. * web/add webdriverIO testing layer This commit adds WebdriverIO as an end-to-end solution to unit testing. WebdriverIO can be run both locally and remotely, supports strong integration with web components, and is generally robust for use in pipelines. I'll confess to working through a tutorial on how to do this for web components, and this is just chapter 2 (I think there are 5 or so chapters...). There's a makefile, with help! If you just run `make` it tells you: ``` Specify a command. The choices are: help Show this help node_modules Runs `npm install` to prepare this feature precommit Run the precommit: spell check all comments, eslint with sonarJS, prettier-write test-good-login Test that we can log into the server. Requires a running instance of the server. test-bad-login Test that bad usernames and passwords create appropriate error messages ``` ... because Makefiles are documentation, and documentation belongs in Makefiles. I've chosen to go with a PageObject-oriented low-level DSL; what that means is that for each major components (a page, a form, a wizard), there's a class that provides human-readable names for human-interactable and human-viewable objects on the page. The LoginPage object, for example, has selectors for the username, password, submit button, and the failure alert; accessing those allows us to test for items as expected., and to write a DSL for "a good login" that's as straightforward as: ``` await LoginPage.open(); await LoginPage.login("ken@goauthentik.io", "eat10bugs"); await expect(UserLibraryPage.pageHeader).toHaveText("My applications"); ``` There was a *lot* of messing around with the LoginPage to get the username and password into the system. For example, I had to do this with all the `waitForClickable` and `waitForEnable` because we both keep the buttons inaccessible until the form has something and we "black out" the page (put a darkening filter over it) while accessing the flow, meaning there was a race condition such that the test would attempt to interact with the username or password field before it was accessible. But this works now, which is very nice. ``` JavaScript get inputUsername() { return $('>>>input[name="uidField"]'); } get btnSubmit() { return $('>>>button[type="submit"]'); } async username(username: string) { await this.inputUsername.waitForClickable(); await this.inputUsername.setValue(username); await this.btnSubmit.waitForEnabled(); await this.btnSubmit.click(); } ``` The bells & whistles of *Prettier*, *Eslint*, and *Codespell* have also been enabled. I do like my guardrails. * web/adding tests: added comments and cleaned up some administrative features. * web/test: changed the name of one test to reflect it's 'good' status * core/allow alternative postgres credentials This commit allows the `dev-reset` command in the Makefile to pick up and use credentials from the `.env` file if they are present, or fallback to the defaults provided if they are not. This is the only place in the Makefile where the database credentials are used directly against postgresql binaries. The syntax was tested with bash, zsh, and csh, and did not fail under those. The `$${:-}` syntax is a combination of a Makefile idiom for "Pass a single `$` to the environment where this command will be executed," and the shell expresion `${VARIABLE:-default}` means "dereference the environment variable; if it is undefined, used the default value provided." * Re-arrange sequence to avoid recursive make. Nothing wrong with recursive make; it just wasn't essential here. `migrate` is just a build target, not a task. * Cleanup according to the Usage: checkmake [options] <makefile>... checkmake -h | --help checkmake --version checkmake --list-rules Makefile linting tool. * core: added 'help' to the Makefile * get postgres config from authentik config loader Signed-off-by: Jens Langhammer <jens@goauthentik.io> * don't set -x by default Signed-off-by: Jens Langhammer <jens@goauthentik.io> * sort help Signed-off-by: Jens Langhammer <jens@goauthentik.io> * update help strings Signed-off-by: Jens Langhammer <jens@goauthentik.io> * web: test LDAP wizard sequence * web: improve testing by adding test admin user via blueprint * This commit continues the application wizard buildout. In this commit are the following changes: - Added SCIM to the list of available providers - Fixed ForwardProxy so that its mode is set correctly. (This is a special case in the committer; I'm unhappy with that.) - Fixed the commit messages so that: - icons are set correctly (Success, Danger, Working) - icons are colored correctly according to state - commit message includes a `data-commit-state` field so tests can find it! - Merged the application wizard tests into a single test pass - Isolated common parts of the application wizard tests to reduce unnecessary repetition. All application tests are the same until you reach the provider section anyway. - Fixed the unit tests so they're finding the right error messages and are enabled to display them correctly. - Moved the test Form handlers into their own folder so they're not cluttering up the Pages folder. * web: add radius to application wizard This commit continues the application wizard buildout. In this commit are the following changes: - Fixed a width-setting bug in the Makefile `make help` feature (i.e "automate that stuff!") - Added Radius to the list of providers we can offer via the wizard - Added `launchUrl` and `UI Settings` to features of the application page the wizard can find - Changed 'SAML Manual Configuration' to just say "SAML Configuration" - Modified `ak-form-group` to take and honor the `aria-label` property (which in turn makes it easier to target specific forms with unit testing) - Reduced the log level for wdio to 'warn'; 'info' was super-spammy and not helpful. It can be put back with `--logLevel info` from the command line. * fix blueprints Signed-off-by: Jens Langhammer <jens@goauthentik.io> * update package name Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add dependabot Signed-off-by: Jens Langhammer <jens@goauthentik.io> * prettier run Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add basic CI Signed-off-by: Jens Langhammer <jens@goauthentik.io> * remove hooks Signed-off-by: Jens Langhammer <jens@goauthentik.io> * web: application wizard refactor & completion This commit refactors the various components of the Wizard and ApplicationWizard, creating a much more maintainable and satisfying Wizard experience for both developers (i.e, *me* and *Jens* so far), and for the customer. The Wizard base has been refactored into three components: **AkWizardController** The `AkWizardController` provides the event listenters for the wizard; it hooks them up, recevies the events, and forwards them to the wizard. It unwraps the event objects and forwards the relevant messages contained in the events. It knows of three event categories: - Navigation requests (move to a different step) - Update requests (the current step has updated the business content) - Close requests (close or cancel the wizard). **ak-wizard-frame** The `ak-wizard-frame` is the ModalButton interface. It provides the Header, Breadcrumbs (nee` "navigation block"), Buttons, and a DIV into which the main content is rendered. **AkWizard** `AkWizard` is an *incomplete* implementation of the wizard. It's meant to be inherited by a child class, which will implement the rest. It extends `AKElement`. It provides the basic content needed, such as steps, currentStep (as an index), an accessor for the step itself, an accessor for the frame, and the interface to the `AkWizardController`. **ApplicationWizard** The `ApplicationWizard` itself has been refactored to accommodate these changes. It inherits from `AkWizard` and provides the business logic for what to do when a form updates, some custom logic for preventing moving through the wizard when the forms are incomplete, and a persistence layer for filling out different providers in the same session. It's simplified a *lot*. The types specified for `AkWizard` are pretty nifty, I think. I could wish the types being passed via the custom events were more robust, but [strongly typed custom events](https://github.com/lit/lit-element/issues/808) turn out to be quite the pain in the, er, neck. As it is, the `precommit` pass did very good at preventing the worst disasters. The steps themselves were re-written as objects so that they could take advantage of their `valid` and `disabled` states and provide more meaningful buttons and labels. I think it's a solid compromise, and it moved a lot of display logic out of the core `handleUpdate()` business method. The tests, such as they are, are passing. * Added comment describing new test. * web: ensuring copy from `main` is canon * web: fixes after merge * web: laying the groundwork for future expansion This commit is a hodge-podge of updates and changes to the web. Functional changes: - Makefile: Fixed a bug in the `help` section that prevented the WIDTH from being accurately calculated if `help` was included rather than in-lined. - ESLint: Modified the "unused vars" rule so that variables starting with an underline are not considered by the rule. This allows for elided variables in event handlers. It's not a perfect solution-- a better one would be to use Typescript's function-specialization typing, but there are too many places where we elide or ignore some variables in a function's usage that switching over to specialization would be a huge lift. - locale: It turns out, lit-locale does its own context management. We don't need to have a context at all in this space, and that's one less listener we need to attach t othe DOM. - ModalButton: A small thing, but using `nothing` instead of "html``" allows lit better control over rendering and reduces the number of actual renders of the page. - FormGroup: Provided a means to modify the aria-label, rather than stick with the just the word "Details." Specializing this field will both help users of screen readers in the future, and will allow test suites to find specific form groups now. - RadioButton: provide a more consistent interface to the RadioButton. First, we dispatch the events to the outside world, and we set the value locally so that the current `Form.ts` continues to behave as expected. We also prevent the "button lost value" event from propagating; this presents a unified select-like interface to users of the RadioButtonGroup. The current value semantics are preserved; other clients of the RadioButton do not see a change in behavior. - EventEmitter: If the custom event detail is *not* an object, do not use the object-like semantics for forwarding it; just send it as-is. - Comments: In the course of laying the groundwork for the application wizard, I throw a LOT of comments into the code, describing APIs, interfaces, class and function signatures, to better document the behavior inside and as signposts for future work. * web: permit arrays to be sent in custom events without interpolation. * actually use assignValue or rather serializeFieldRecursive Signed-off-by: Jens Langhammer <jens@goauthentik.io> * web: eslint & prettier fixes, plus small aesthetic differences. * Restoring this file. Not sure where it disappears to. * fix label in dark mode Signed-off-by: Jens Langhammer <jens@goauthentik.io> * SCIM Manuel -> SCIM Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix lint errors Signed-off-by: Jens Langhammer <jens@goauthentik.io> * web: better converter configuration, CSS repair, and forward-domain-proxy 1. Forward Domain Proxy. I wasn't sure if this method was appropriate for the wizard, but Jens says it is. I've added it. 2. In the process of doing so, I decided that the Provider.converter field was overly complexified; I tried too hard to reduce the number of functions I needed to define, but in the process outsourced some of the logic of converting the Wizard's dataset into a property typed request to the `commit` phase, which was inappropriate. All of the logic about a provider, aside from its display, should be here with the code that distinguishes between providers. This commit makes it so. 3. Small CSS fix: the fields inherited from the Proxy provider forms had some unexpected CSS which was causing a bit of a weird indent. That has been rectified. * web: running pre-commit after merge. * web: ensure the applications wizard tests finish after current changes * prettier has opinions. * web: application wizard spit & polish The "ApplicationWizardHint" now correctly uses the localstorage and allows the user to navigate back and see the message after it's been hidden, so that it will always be available during the test phase. The ApplicationList's old "Create Application Form" button has been restored for the purposes of the test phase. The ApplicationWizard is now available on both the ApplicationList and ProviderList pages. Tana and I discussed the microcopy, putting a stronger second-person "You can do..." twist onto the language, to give the user the sense of empowerment. The ShowHintController now has both "hide" and "show" operations, to support the hint restoration. * web: updated storybook stories for the wizard, illustration how "a simple wizard" is configured in source code and tested with storybook. * web: I hate getting spanked by prettier. * web: sometimes I wish I had lower standards Anyway, this was a very stupid bug, because by definition function definition arguments don't have uses, they're being defined, not implemented. Fixed, conf fixed to compensate, and consequences conquered. * move context from labs to main Signed-off-by: Jens Langhammer <jens@goauthentik.io> * Revert "move context from labs to main" This reverts commit3718ee6904. * web: reify the data loop I was very unhappy with the "update this dot-path" mechanism I was using earlier; it was hard for me to read and understand what was happening, and I wrote the darned thing. I decided instead to go with a hard substitution model; each phase of the wizard is responsible for updating the *entire* payload, mostly by creating a new payload and substituting the field value associated with the event. On the receiver, we have to do that *again* to handle the swapping of providers when the user chooses one and then another. It looks clunky, and it is, but it's *legible*; a junior dev could understand what it's doing, and that's the goal. * Revert "web: reify the data loop" This reverts commit09fedcacf0. * web: revert the 'lit' to 'lit-labs' for task and context. --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io> Co-authored-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
		@ -1,64 +0,0 @@
 | 
			
		||||
import "@goauthentik/admin/applications/wizard/InitialApplicationWizardPage";
 | 
			
		||||
import "@goauthentik/admin/applications/wizard/TypeApplicationWizardPage";
 | 
			
		||||
import "@goauthentik/admin/applications/wizard/ldap/TypeLDAPApplicationWizardPage";
 | 
			
		||||
import "@goauthentik/admin/applications/wizard/link/TypeLinkApplicationWizardPage";
 | 
			
		||||
import "@goauthentik/admin/applications/wizard/oauth/TypeOAuthAPIApplicationWizardPage";
 | 
			
		||||
import "@goauthentik/admin/applications/wizard/oauth/TypeOAuthApplicationWizardPage";
 | 
			
		||||
import "@goauthentik/admin/applications/wizard/oauth/TypeOAuthCodeApplicationWizardPage";
 | 
			
		||||
import "@goauthentik/admin/applications/wizard/oauth/TypeOAuthImplicitApplicationWizardPage";
 | 
			
		||||
import "@goauthentik/admin/applications/wizard/proxy/TypeProxyApplicationWizardPage";
 | 
			
		||||
import "@goauthentik/admin/applications/wizard/saml/TypeSAMLApplicationWizardPage";
 | 
			
		||||
import "@goauthentik/admin/applications/wizard/saml/TypeSAMLConfigApplicationWizardPage";
 | 
			
		||||
import "@goauthentik/admin/applications/wizard/saml/TypeSAMLImportApplicationWizardPage";
 | 
			
		||||
import { AKElement } from "@goauthentik/elements/Base";
 | 
			
		||||
import "@goauthentik/elements/wizard/Wizard";
 | 
			
		||||
 | 
			
		||||
import { msg } from "@lit/localize";
 | 
			
		||||
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
 | 
			
		||||
import { CSSResult, TemplateResult, html } from "lit";
 | 
			
		||||
import { property } from "lit/decorators.js";
 | 
			
		||||
 | 
			
		||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
 | 
			
		||||
import PFRadio from "@patternfly/patternfly/components/Radio/radio.css";
 | 
			
		||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
 | 
			
		||||
 | 
			
		||||
@customElement("ak-application-wizard")
 | 
			
		||||
export class ApplicationWizard extends AKElement {
 | 
			
		||||
    static get styles(): CSSResult[] {
 | 
			
		||||
        return [PFBase, PFButton, PFRadio];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @property({ type: Boolean })
 | 
			
		||||
    open = false;
 | 
			
		||||
 | 
			
		||||
    @property()
 | 
			
		||||
    createText = msg("Create");
 | 
			
		||||
 | 
			
		||||
    @property({ type: Boolean })
 | 
			
		||||
    showButton = true;
 | 
			
		||||
 | 
			
		||||
    @property({ attribute: false })
 | 
			
		||||
    finalHandler: () => Promise<void> = () => {
 | 
			
		||||
        return Promise.resolve();
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    render(): TemplateResult {
 | 
			
		||||
        return html`
 | 
			
		||||
            <ak-wizard
 | 
			
		||||
                .open=${this.open}
 | 
			
		||||
                .steps=${["ak-application-wizard-initial", "ak-application-wizard-type"]}
 | 
			
		||||
                header=${msg("New application")}
 | 
			
		||||
                description=${msg("Create a new application.")}
 | 
			
		||||
                .finalHandler=${() => {
 | 
			
		||||
                    return this.finalHandler();
 | 
			
		||||
                }}
 | 
			
		||||
            >
 | 
			
		||||
                ${this.showButton
 | 
			
		||||
                    ? html`<button slot="trigger" class="pf-c-button pf-m-primary">
 | 
			
		||||
                          ${this.createText}
 | 
			
		||||
                      </button>`
 | 
			
		||||
                    : html``}
 | 
			
		||||
            </ak-wizard>
 | 
			
		||||
        `;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										28
									
								
								web/src/admin/applications/wizard/BasePanel.css.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								web/src/admin/applications/wizard/BasePanel.css.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,28 @@
 | 
			
		||||
import { css } from "lit";
 | 
			
		||||
 | 
			
		||||
import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
 | 
			
		||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
 | 
			
		||||
import PFCard from "@patternfly/patternfly/components/Card/card.css";
 | 
			
		||||
import PFForm from "@patternfly/patternfly/components/Form/form.css";
 | 
			
		||||
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
 | 
			
		||||
import PFInputGroup from "@patternfly/patternfly/components/InputGroup/input-group.css";
 | 
			
		||||
import PFRadio from "@patternfly/patternfly/components/Radio/radio.css";
 | 
			
		||||
import PFSwitch from "@patternfly/patternfly/components/Switch/switch.css";
 | 
			
		||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
 | 
			
		||||
 | 
			
		||||
export const styles = [
 | 
			
		||||
    PFBase,
 | 
			
		||||
    PFCard,
 | 
			
		||||
    PFButton,
 | 
			
		||||
    PFForm,
 | 
			
		||||
    PFAlert,
 | 
			
		||||
    PFRadio,
 | 
			
		||||
    PFInputGroup,
 | 
			
		||||
    PFFormControl,
 | 
			
		||||
    PFSwitch,
 | 
			
		||||
    css`
 | 
			
		||||
        select[multiple] {
 | 
			
		||||
            height: 15em;
 | 
			
		||||
        }
 | 
			
		||||
    `,
 | 
			
		||||
];
 | 
			
		||||
							
								
								
									
										35
									
								
								web/src/admin/applications/wizard/BasePanel.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								web/src/admin/applications/wizard/BasePanel.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,35 @@
 | 
			
		||||
import { WizardPanel } from "@goauthentik/components/ak-wizard-main/types";
 | 
			
		||||
import { AKElement } from "@goauthentik/elements/Base";
 | 
			
		||||
import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter";
 | 
			
		||||
 | 
			
		||||
import { consume } from "@lit-labs/context";
 | 
			
		||||
import { query } from "@lit/reactive-element/decorators.js";
 | 
			
		||||
 | 
			
		||||
import { styles as AwadStyles } from "./BasePanel.css";
 | 
			
		||||
 | 
			
		||||
import { applicationWizardContext } from "./ContextIdentity";
 | 
			
		||||
import type { ApplicationWizardState, ApplicationWizardStateUpdate } from "./types";
 | 
			
		||||
 | 
			
		||||
export class ApplicationWizardPageBase
 | 
			
		||||
    extends CustomEmitterElement(AKElement)
 | 
			
		||||
    implements WizardPanel
 | 
			
		||||
{
 | 
			
		||||
    static get styles() {
 | 
			
		||||
        return AwadStyles;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @query("form")
 | 
			
		||||
    form!: HTMLFormElement;
 | 
			
		||||
 | 
			
		||||
    rendered = false;
 | 
			
		||||
 | 
			
		||||
    @consume({ context: applicationWizardContext })
 | 
			
		||||
    public wizard!: ApplicationWizardState;
 | 
			
		||||
 | 
			
		||||
    // This used to be more complex; now it just establishes the event name.
 | 
			
		||||
    dispatchWizardUpdate(update: ApplicationWizardStateUpdate) {
 | 
			
		||||
        this.dispatchCustomEvent("ak-wizard-update", update);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default ApplicationWizardPageBase;
 | 
			
		||||
							
								
								
									
										9
									
								
								web/src/admin/applications/wizard/ContextIdentity.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								web/src/admin/applications/wizard/ContextIdentity.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,9 @@
 | 
			
		||||
import { createContext } from "@lit-labs/context";
 | 
			
		||||
 | 
			
		||||
import { ApplicationWizardState } from "./types";
 | 
			
		||||
 | 
			
		||||
export const applicationWizardContext = createContext<ApplicationWizardState>(
 | 
			
		||||
    Symbol("ak-application-wizard-state-context"),
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export default applicationWizardContext;
 | 
			
		||||
@ -1,75 +0,0 @@
 | 
			
		||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
 | 
			
		||||
import { convertToSlug } from "@goauthentik/common/utils";
 | 
			
		||||
import { KeyUnknown } from "@goauthentik/elements/forms/Form";
 | 
			
		||||
import "@goauthentik/elements/forms/FormGroup";
 | 
			
		||||
import "@goauthentik/elements/forms/HorizontalFormElement";
 | 
			
		||||
import { WizardFormPage } from "@goauthentik/elements/wizard/WizardFormPage";
 | 
			
		||||
 | 
			
		||||
import { msg } from "@lit/localize";
 | 
			
		||||
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
 | 
			
		||||
import { TemplateResult, html } from "lit";
 | 
			
		||||
 | 
			
		||||
import { ApplicationRequest, CoreApi, Provider } from "@goauthentik/api";
 | 
			
		||||
 | 
			
		||||
@customElement("ak-application-wizard-initial")
 | 
			
		||||
export class InitialApplicationWizardPage extends WizardFormPage {
 | 
			
		||||
    sidebarLabel = () => msg("Application details");
 | 
			
		||||
 | 
			
		||||
    nextDataCallback = async (data: KeyUnknown): Promise<boolean> => {
 | 
			
		||||
        const name = data.name as string;
 | 
			
		||||
        let slug = convertToSlug(name || "");
 | 
			
		||||
        // Check if an application with the generated slug already exists
 | 
			
		||||
        const apps = await new CoreApi(DEFAULT_CONFIG).coreApplicationsList({
 | 
			
		||||
            search: slug,
 | 
			
		||||
        });
 | 
			
		||||
        if (apps.results.filter((app) => app.slug == slug)) {
 | 
			
		||||
            slug += "-1";
 | 
			
		||||
        }
 | 
			
		||||
        this.host.state["slug"] = slug;
 | 
			
		||||
        this.host.state["name"] = name;
 | 
			
		||||
        this.host.addActionBefore(msg("Create application"), async (): Promise<boolean> => {
 | 
			
		||||
            const req: ApplicationRequest = {
 | 
			
		||||
                name: name || "",
 | 
			
		||||
                slug: slug,
 | 
			
		||||
                metaPublisher: data.metaPublisher as string,
 | 
			
		||||
                metaDescription: data.metaDescription as string,
 | 
			
		||||
            };
 | 
			
		||||
            if ("provider" in this.host.state) {
 | 
			
		||||
                req.provider = (this.host.state["provider"] as Provider).pk;
 | 
			
		||||
            }
 | 
			
		||||
            if ("link" in this.host.state) {
 | 
			
		||||
                req.metaLaunchUrl = this.host.state["link"] as string;
 | 
			
		||||
            }
 | 
			
		||||
            this.host.state["app"] = await new CoreApi(DEFAULT_CONFIG).coreApplicationsCreate({
 | 
			
		||||
                applicationRequest: req,
 | 
			
		||||
            });
 | 
			
		||||
            return true;
 | 
			
		||||
        });
 | 
			
		||||
        return true;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    renderForm(): TemplateResult {
 | 
			
		||||
        return html`
 | 
			
		||||
            <form class="pf-c-form pf-m-horizontal">
 | 
			
		||||
                <ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">
 | 
			
		||||
                    <input type="text" value="" class="pf-c-form-control" required />
 | 
			
		||||
                    <p class="pf-c-form__helper-text">${msg("Application's display Name.")}</p>
 | 
			
		||||
                </ak-form-element-horizontal>
 | 
			
		||||
                <ak-form-group ?expanded=${true}>
 | 
			
		||||
                    <span slot="header"> ${msg("Additional UI settings")} </span>
 | 
			
		||||
                    <div slot="body" class="pf-c-form">
 | 
			
		||||
                        <ak-form-element-horizontal
 | 
			
		||||
                            label=${msg("Description")}
 | 
			
		||||
                            name="metaDescription"
 | 
			
		||||
                        >
 | 
			
		||||
                            <textarea class="pf-c-form-control"></textarea>
 | 
			
		||||
                        </ak-form-element-horizontal>
 | 
			
		||||
                        <ak-form-element-horizontal label=${msg("Publisher")} name="metaPublisher">
 | 
			
		||||
                            <input type="text" value="" class="pf-c-form-control" />
 | 
			
		||||
                        </ak-form-element-horizontal>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </ak-form-group>
 | 
			
		||||
            </form>
 | 
			
		||||
        `;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,85 +0,0 @@
 | 
			
		||||
import { WizardPage } from "@goauthentik/elements/wizard/WizardPage";
 | 
			
		||||
 | 
			
		||||
import { msg } from "@lit/localize";
 | 
			
		||||
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
 | 
			
		||||
import { CSSResult, TemplateResult, html } from "lit";
 | 
			
		||||
 | 
			
		||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
 | 
			
		||||
import PFForm from "@patternfly/patternfly/components/Form/form.css";
 | 
			
		||||
import PFRadio from "@patternfly/patternfly/components/Radio/radio.css";
 | 
			
		||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
 | 
			
		||||
 | 
			
		||||
import { TypeCreate } from "@goauthentik/api";
 | 
			
		||||
 | 
			
		||||
@customElement("ak-application-wizard-type")
 | 
			
		||||
export class TypeApplicationWizardPage extends WizardPage {
 | 
			
		||||
    applicationTypes: TypeCreate[] = [
 | 
			
		||||
        {
 | 
			
		||||
            component: "ak-application-wizard-type-oauth",
 | 
			
		||||
            name: msg("OAuth2/OIDC"),
 | 
			
		||||
            description: msg("Modern applications, APIs and Single-page applications."),
 | 
			
		||||
            modelName: "",
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            component: "ak-application-wizard-type-saml",
 | 
			
		||||
            name: msg("SAML"),
 | 
			
		||||
            description: msg(
 | 
			
		||||
                "XML-based SSO standard. Use this if your application only supports SAML.",
 | 
			
		||||
            ),
 | 
			
		||||
            modelName: "",
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            component: "ak-application-wizard-type-proxy",
 | 
			
		||||
            name: msg("Proxy"),
 | 
			
		||||
            description: msg("Legacy applications which don't natively support SSO."),
 | 
			
		||||
            modelName: "",
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            component: "ak-application-wizard-type-ldap",
 | 
			
		||||
            name: msg("LDAP"),
 | 
			
		||||
            description: msg(
 | 
			
		||||
                "Provide an LDAP interface for applications and users to authenticate against.",
 | 
			
		||||
            ),
 | 
			
		||||
            modelName: "",
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            component: "ak-application-wizard-type-link",
 | 
			
		||||
            name: msg("Link"),
 | 
			
		||||
            description: msg(
 | 
			
		||||
                "Provide an LDAP interface for applications and users to authenticate against.",
 | 
			
		||||
            ),
 | 
			
		||||
            modelName: "",
 | 
			
		||||
        },
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    sidebarLabel = () => msg("Authentication method");
 | 
			
		||||
 | 
			
		||||
    static get styles(): CSSResult[] {
 | 
			
		||||
        return [PFBase, PFButton, PFForm, PFRadio];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render(): TemplateResult {
 | 
			
		||||
        return html`<form class="pf-c-form pf-m-horizontal">
 | 
			
		||||
            ${this.applicationTypes.map((type) => {
 | 
			
		||||
                return html`<div class="pf-c-radio">
 | 
			
		||||
                    <input
 | 
			
		||||
                        class="pf-c-radio__input"
 | 
			
		||||
                        type="radio"
 | 
			
		||||
                        name="type"
 | 
			
		||||
                        id=${type.component}
 | 
			
		||||
                        @change=${() => {
 | 
			
		||||
                            this.host.steps = [
 | 
			
		||||
                                "ak-application-wizard-initial",
 | 
			
		||||
                                "ak-application-wizard-type",
 | 
			
		||||
                                type.component,
 | 
			
		||||
                            ];
 | 
			
		||||
                            this.host.isValid = true;
 | 
			
		||||
                        }}
 | 
			
		||||
                    />
 | 
			
		||||
                    <label class="pf-c-radio__label" for=${type.component}>${type.name}</label>
 | 
			
		||||
                    <span class="pf-c-radio__description">${type.description}</span>
 | 
			
		||||
                </div>`;
 | 
			
		||||
            })}
 | 
			
		||||
        </form>`;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										124
									
								
								web/src/admin/applications/wizard/ak-application-wizard.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								web/src/admin/applications/wizard/ak-application-wizard.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,124 @@
 | 
			
		||||
import { merge } from "@goauthentik/common/merge";
 | 
			
		||||
import { AkWizard } from "@goauthentik/components/ak-wizard-main/AkWizard";
 | 
			
		||||
import { CustomListenerElement } from "@goauthentik/elements/utils/eventEmitter";
 | 
			
		||||
 | 
			
		||||
import { ContextProvider } from "@lit-labs/context";
 | 
			
		||||
import { msg } from "@lit/localize";
 | 
			
		||||
import { customElement, state } from "lit/decorators.js";
 | 
			
		||||
 | 
			
		||||
import applicationWizardContext from "./ContextIdentity";
 | 
			
		||||
import { newSteps } from "./steps";
 | 
			
		||||
import {
 | 
			
		||||
    ApplicationStep,
 | 
			
		||||
    ApplicationWizardState,
 | 
			
		||||
    ApplicationWizardStateUpdate,
 | 
			
		||||
    OneOfProvider,
 | 
			
		||||
} from "./types";
 | 
			
		||||
 | 
			
		||||
const freshWizardState = () => ({
 | 
			
		||||
    providerModel: "",
 | 
			
		||||
    app: {},
 | 
			
		||||
    provider: {},
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@customElement("ak-application-wizard")
 | 
			
		||||
export class ApplicationWizard extends CustomListenerElement(
 | 
			
		||||
    AkWizard<ApplicationWizardStateUpdate, ApplicationStep>,
 | 
			
		||||
) {
 | 
			
		||||
    constructor() {
 | 
			
		||||
        super(msg("Create With Wizard"), msg("New application"), msg("Create a new application"));
 | 
			
		||||
        this.steps = newSteps();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * We're going to be managing the content of the forms by percolating all of the data up to this
 | 
			
		||||
     * class, which will ultimately transmit all of it to the server as a transaction. The
 | 
			
		||||
     * WizardFramework doesn't know anything about the nature of the data itself; it just forwards
 | 
			
		||||
     * valid updates to us. So here we maintain a state object *and* update it so all child
 | 
			
		||||
     * components can access the wizard state.
 | 
			
		||||
     *
 | 
			
		||||
     */
 | 
			
		||||
    @state()
 | 
			
		||||
    wizardState: ApplicationWizardState = freshWizardState();
 | 
			
		||||
 | 
			
		||||
    wizardStateProvider = new ContextProvider(this, {
 | 
			
		||||
        context: applicationWizardContext,
 | 
			
		||||
        initialValue: this.wizardState,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * One of our steps has multiple display variants, one for each type of service provider. We
 | 
			
		||||
     * want to *preserve* a customer's decisions about different providers; never make someone "go
 | 
			
		||||
     * back and type it all back in," even if it's probably rare that someone will chose one
 | 
			
		||||
     * provider, realize it's the wrong one, and go back to chose a different one, *and then go
 | 
			
		||||
     * back*. Nonetheless, strive to *never* lose customer input.
 | 
			
		||||
     *
 | 
			
		||||
     */
 | 
			
		||||
    providerCache: Map<string, OneOfProvider> = new Map();
 | 
			
		||||
 | 
			
		||||
    maybeProviderSwap(providerModel: string | undefined): boolean {
 | 
			
		||||
        if (
 | 
			
		||||
            providerModel === undefined ||
 | 
			
		||||
            typeof providerModel !== "string" ||
 | 
			
		||||
            providerModel === this.wizardState.providerModel
 | 
			
		||||
        ) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.providerCache.set(this.wizardState.providerModel, this.wizardState.provider);
 | 
			
		||||
        const prevProvider = this.providerCache.get(providerModel);
 | 
			
		||||
        this.wizardState.provider = prevProvider ?? {
 | 
			
		||||
            name: `Provider for ${this.wizardState.app.name}`,
 | 
			
		||||
        };
 | 
			
		||||
        const method = this.steps.find(({ id }) => id === "provider-details");
 | 
			
		||||
        if (!method) {
 | 
			
		||||
            throw new Error("Could not find Authentication Method page?");
 | 
			
		||||
        }
 | 
			
		||||
        method.disabled = false;
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // And this is where all the special cases go...
 | 
			
		||||
    handleUpdate(detail: ApplicationWizardStateUpdate) {
 | 
			
		||||
        if (detail.status === "submitted") {
 | 
			
		||||
            this.step.valid = true;
 | 
			
		||||
            this.requestUpdate();
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.step.valid = this.step.valid || detail.status === "valid";
 | 
			
		||||
 | 
			
		||||
        const update = detail.update;
 | 
			
		||||
        if (!update) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (this.maybeProviderSwap(update.providerModel)) {
 | 
			
		||||
            this.requestUpdate();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.wizardState = merge(this.wizardState, update) as ApplicationWizardState;
 | 
			
		||||
        this.wizardStateProvider.setValue(this.wizardState);
 | 
			
		||||
        this.requestUpdate();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    close() {
 | 
			
		||||
        this.steps = newSteps();
 | 
			
		||||
        this.currentStep = 0;
 | 
			
		||||
        this.wizardState = freshWizardState();
 | 
			
		||||
        this.providerCache = new Map();
 | 
			
		||||
        this.wizardStateProvider.setValue(this.wizardState);
 | 
			
		||||
        this.frame.value!.open = false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    handleNav(stepId: number | undefined) {
 | 
			
		||||
        if (stepId === undefined || this.steps[stepId] === undefined) {
 | 
			
		||||
            throw new Error(`Attempt to navigate to undefined step: ${stepId}`);
 | 
			
		||||
        }
 | 
			
		||||
        if (stepId > this.currentStep && !this.step.valid) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        this.currentStep = stepId;
 | 
			
		||||
        this.requestUpdate();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,101 @@
 | 
			
		||||
import { policyOptions } from "@goauthentik/admin/applications/ApplicationForm";
 | 
			
		||||
import { first } from "@goauthentik/common/utils";
 | 
			
		||||
import "@goauthentik/components/ak-radio-input";
 | 
			
		||||
import "@goauthentik/components/ak-slug-input";
 | 
			
		||||
import "@goauthentik/components/ak-switch-input";
 | 
			
		||||
import "@goauthentik/components/ak-text-input";
 | 
			
		||||
import "@goauthentik/elements/forms/FormGroup";
 | 
			
		||||
import "@goauthentik/elements/forms/FormGroup";
 | 
			
		||||
import "@goauthentik/elements/forms/HorizontalFormElement";
 | 
			
		||||
 | 
			
		||||
import { msg } from "@lit/localize";
 | 
			
		||||
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
 | 
			
		||||
import { TemplateResult, html } from "lit";
 | 
			
		||||
import { ifDefined } from "lit/directives/if-defined.js";
 | 
			
		||||
 | 
			
		||||
import BasePanel from "../BasePanel";
 | 
			
		||||
 | 
			
		||||
@customElement("ak-application-wizard-application-details")
 | 
			
		||||
export class ApplicationWizardApplicationDetails extends BasePanel {
 | 
			
		||||
    handleChange(ev: Event) {
 | 
			
		||||
        if (!ev.target) {
 | 
			
		||||
            console.warn(`Received event with no target: ${ev}`);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const target = ev.target as HTMLInputElement;
 | 
			
		||||
        const value = target.type === "checkbox" ? target.checked : target.value;
 | 
			
		||||
        this.dispatchWizardUpdate({
 | 
			
		||||
            update: {
 | 
			
		||||
                app: {
 | 
			
		||||
                    [target.name]: value,
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
            status: this.form.checkValidity() ? "valid" : "invalid",
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    validator() {
 | 
			
		||||
        return this.form.reportValidity();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render(): TemplateResult {
 | 
			
		||||
        return html` <form class="pf-c-form pf-m-horizontal" @input=${this.handleChange}>
 | 
			
		||||
            <ak-text-input
 | 
			
		||||
                name="name"
 | 
			
		||||
                value=${ifDefined(this.wizard.app?.name)}
 | 
			
		||||
                label=${msg("Name")}
 | 
			
		||||
                required
 | 
			
		||||
                help=${msg("Application's display Name.")}
 | 
			
		||||
                id="ak-application-wizard-details-name"
 | 
			
		||||
            ></ak-text-input>
 | 
			
		||||
            <ak-slug-input
 | 
			
		||||
                name="slug"
 | 
			
		||||
                value=${ifDefined(this.wizard.app?.slug)}
 | 
			
		||||
                label=${msg("Slug")}
 | 
			
		||||
                source="#ak-application-wizard-details-name"
 | 
			
		||||
                required
 | 
			
		||||
                help=${msg("Internal application name used in URLs.")}
 | 
			
		||||
            ></ak-slug-input>
 | 
			
		||||
            <ak-text-input
 | 
			
		||||
                name="group"
 | 
			
		||||
                value=${ifDefined(this.wizard.app?.group)}
 | 
			
		||||
                label=${msg("Group")}
 | 
			
		||||
                help=${msg(
 | 
			
		||||
                    "Optionally enter a group name. Applications with identical groups are shown grouped together.",
 | 
			
		||||
                )}
 | 
			
		||||
            ></ak-text-input>
 | 
			
		||||
            <ak-radio-input
 | 
			
		||||
                label=${msg("Policy engine mode")}
 | 
			
		||||
                required
 | 
			
		||||
                name="policyEngineMode"
 | 
			
		||||
                .options=${policyOptions}
 | 
			
		||||
                .value=${this.wizard.app?.policyEngineMode}
 | 
			
		||||
            ></ak-radio-input>
 | 
			
		||||
            <ak-form-group aria-label="UI Settings">
 | 
			
		||||
                <span slot="header"> ${msg("UI Settings")} </span>
 | 
			
		||||
                <div slot="body" class="pf-c-form">
 | 
			
		||||
                    <ak-text-input
 | 
			
		||||
                        name="metaLaunchUrl"
 | 
			
		||||
                        label=${msg("Launch URL")}
 | 
			
		||||
                        value=${ifDefined(this.wizard.app?.metaLaunchUrl)}
 | 
			
		||||
                        help=${msg(
 | 
			
		||||
                            "If left empty, authentik will try to extract the launch URL based on the selected provider.",
 | 
			
		||||
                        )}
 | 
			
		||||
                    ></ak-text-input>
 | 
			
		||||
                    <ak-switch-input
 | 
			
		||||
                        name="openInNewTab"
 | 
			
		||||
                        ?checked=${first(this.wizard.app?.openInNewTab, false)}
 | 
			
		||||
                        label=${msg("Open in new tab")}
 | 
			
		||||
                        help=${msg(
 | 
			
		||||
                            "If checked, the launch URL will open in a new browser tab or window from the user's application library.",
 | 
			
		||||
                        )}
 | 
			
		||||
                    >
 | 
			
		||||
                    </ak-switch-input>
 | 
			
		||||
                </div>
 | 
			
		||||
            </ak-form-group>
 | 
			
		||||
        </form>`;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default ApplicationWizardApplicationDetails;
 | 
			
		||||
@ -0,0 +1,161 @@
 | 
			
		||||
import { msg } from "@lit/localize";
 | 
			
		||||
import { TemplateResult, html } from "lit";
 | 
			
		||||
 | 
			
		||||
import type { ProviderModelEnum as ProviderModelEnumType, TypeCreate } from "@goauthentik/api";
 | 
			
		||||
import { ProviderModelEnum, ProxyMode } from "@goauthentik/api";
 | 
			
		||||
import type {
 | 
			
		||||
    LDAPProviderRequest,
 | 
			
		||||
    ModelRequest,
 | 
			
		||||
    OAuth2ProviderRequest,
 | 
			
		||||
    ProxyProviderRequest,
 | 
			
		||||
    RadiusProviderRequest,
 | 
			
		||||
    SAMLProviderRequest,
 | 
			
		||||
    SCIMProviderRequest,
 | 
			
		||||
} from "@goauthentik/api";
 | 
			
		||||
 | 
			
		||||
import { OneOfProvider } from "../types";
 | 
			
		||||
 | 
			
		||||
type ProviderRenderer = () => TemplateResult;
 | 
			
		||||
 | 
			
		||||
type ModelConverter = (provider: OneOfProvider) => ModelRequest;
 | 
			
		||||
 | 
			
		||||
type ProviderType = [
 | 
			
		||||
    string,
 | 
			
		||||
    string,
 | 
			
		||||
    string,
 | 
			
		||||
    ProviderRenderer,
 | 
			
		||||
    ProviderModelEnumType,
 | 
			
		||||
    ModelConverter,
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
export type LocalTypeCreate = TypeCreate & {
 | 
			
		||||
    formName: string;
 | 
			
		||||
    modelName: ProviderModelEnumType;
 | 
			
		||||
    converter: ModelConverter;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// prettier-ignore
 | 
			
		||||
const _providerModelsTable: ProviderType[] = [
 | 
			
		||||
    [
 | 
			
		||||
        "oauth2provider",
 | 
			
		||||
        msg("OAuth2/OpenID"),
 | 
			
		||||
        msg("Modern applications, APIs and Single-page applications."),
 | 
			
		||||
        () => html`<ak-application-wizard-authentication-by-oauth></ak-application-wizard-authentication-by-oauth>`,
 | 
			
		||||
        ProviderModelEnum.Oauth2Oauth2provider,
 | 
			
		||||
        (provider: OneOfProvider) => ({
 | 
			
		||||
            providerModel: ProviderModelEnum.Oauth2Oauth2provider,
 | 
			
		||||
            ...(provider as OAuth2ProviderRequest),
 | 
			
		||||
        }),
 | 
			
		||||
    ],
 | 
			
		||||
    [
 | 
			
		||||
        "ldapprovider",
 | 
			
		||||
        msg("LDAP"),
 | 
			
		||||
        msg("Provide an LDAP interface for applications and users to authenticate against."),
 | 
			
		||||
        () => html`<ak-application-wizard-authentication-by-ldap></ak-application-wizard-authentication-by-ldap>`,
 | 
			
		||||
        ProviderModelEnum.LdapLdapprovider,
 | 
			
		||||
                (provider: OneOfProvider) => ({
 | 
			
		||||
            providerModel: ProviderModelEnum.LdapLdapprovider,
 | 
			
		||||
            ...(provider as LDAPProviderRequest),
 | 
			
		||||
        }),
 | 
			
		||||
 | 
			
		||||
    ],
 | 
			
		||||
    [
 | 
			
		||||
        "proxyprovider-proxy",
 | 
			
		||||
        msg("Transparent Reverse Proxy"),
 | 
			
		||||
        msg("For transparent reverse proxies with required authentication"),
 | 
			
		||||
        () => html`<ak-application-wizard-authentication-for-reverse-proxy></ak-application-wizard-authentication-for-reverse-proxy>`,
 | 
			
		||||
        ProviderModelEnum.ProxyProxyprovider,
 | 
			
		||||
        (provider: OneOfProvider) => ({
 | 
			
		||||
            providerModel: ProviderModelEnum.ProxyProxyprovider,
 | 
			
		||||
            ...(provider as ProxyProviderRequest),
 | 
			
		||||
            mode: ProxyMode.Proxy,
 | 
			
		||||
        }),
 | 
			
		||||
    ],
 | 
			
		||||
    [
 | 
			
		||||
        "proxyprovider-forwardsingle",
 | 
			
		||||
        msg("Forward Auth Single Application"),
 | 
			
		||||
        msg("For nginx's auth_request or traefix's forwardAuth"),
 | 
			
		||||
        () => html`<ak-application-wizard-authentication-for-single-forward-proxy></ak-application-wizard-authentication-for-single-forward-proxy>`,
 | 
			
		||||
        ProviderModelEnum.ProxyProxyprovider  ,
 | 
			
		||||
        (provider: OneOfProvider) => ({
 | 
			
		||||
            providerModel: ProviderModelEnum.ProxyProxyprovider,
 | 
			
		||||
            ...(provider as ProxyProviderRequest),
 | 
			
		||||
            mode: ProxyMode.ForwardSingle,
 | 
			
		||||
        }),
 | 
			
		||||
 | 
			
		||||
    ],
 | 
			
		||||
    [
 | 
			
		||||
        "proxyprovider-forwarddomain",
 | 
			
		||||
        msg("Forward Auth Domain Level"),
 | 
			
		||||
        msg("For nginx's auth_request or traefix's forwardAuth per root domain"),
 | 
			
		||||
        () => html`<ak-application-wizard-authentication-for-forward-proxy-domain></ak-application-wizard-authentication-for-forward-proxy-domain>`,
 | 
			
		||||
        ProviderModelEnum.ProxyProxyprovider  ,
 | 
			
		||||
        (provider: OneOfProvider) => ({
 | 
			
		||||
            providerModel: ProviderModelEnum.ProxyProxyprovider,
 | 
			
		||||
            ...(provider as ProxyProviderRequest),
 | 
			
		||||
            mode: ProxyMode.ForwardDomain,
 | 
			
		||||
        }),
 | 
			
		||||
    ],
 | 
			
		||||
    [
 | 
			
		||||
        "samlprovider",
 | 
			
		||||
        msg("SAML Configuration"),
 | 
			
		||||
        msg("Configure SAML provider manually"),
 | 
			
		||||
        () => html`<ak-application-wizard-authentication-by-saml-configuration></ak-application-wizard-authentication-by-saml-configuration>`,
 | 
			
		||||
        ProviderModelEnum.SamlSamlprovider,
 | 
			
		||||
        (provider: OneOfProvider) => ({
 | 
			
		||||
            providerModel: ProviderModelEnum.SamlSamlprovider,
 | 
			
		||||
            ...(provider as SAMLProviderRequest),
 | 
			
		||||
        }),
 | 
			
		||||
        
 | 
			
		||||
    ],
 | 
			
		||||
    [
 | 
			
		||||
        "radiusprovider",
 | 
			
		||||
        msg("RADIUS Configuration"),
 | 
			
		||||
        msg("Configure RADIUS provider manually"),
 | 
			
		||||
        () => html`<ak-application-wizard-authentication-by-radius></ak-application-wizard-authentication-by-radius>`,
 | 
			
		||||
        ProviderModelEnum.RadiusRadiusprovider,
 | 
			
		||||
                (provider: OneOfProvider) => ({
 | 
			
		||||
            providerModel: ProviderModelEnum.RadiusRadiusprovider,
 | 
			
		||||
            ...(provider as RadiusProviderRequest),
 | 
			
		||||
        }),
 | 
			
		||||
 | 
			
		||||
    ],
 | 
			
		||||
    [
 | 
			
		||||
        "scimprovider",
 | 
			
		||||
        msg("SCIM configuration"),
 | 
			
		||||
        msg("Configure SCIM provider manually"),
 | 
			
		||||
        () => html`<ak-application-wizard-authentication-by-scim></ak-application-wizard-authentication-by-scim>`,
 | 
			
		||||
        ProviderModelEnum.ScimScimprovider,
 | 
			
		||||
                (provider: OneOfProvider) => ({
 | 
			
		||||
            providerModel: ProviderModelEnum.ScimScimprovider,
 | 
			
		||||
            ...(provider as SCIMProviderRequest),
 | 
			
		||||
        }),
 | 
			
		||||
 | 
			
		||||
    ],
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
function mapProviders([
 | 
			
		||||
    formName,
 | 
			
		||||
    name,
 | 
			
		||||
    description,
 | 
			
		||||
    _,
 | 
			
		||||
    modelName,
 | 
			
		||||
    converter,
 | 
			
		||||
]: ProviderType): LocalTypeCreate {
 | 
			
		||||
    return {
 | 
			
		||||
        formName,
 | 
			
		||||
        name,
 | 
			
		||||
        description,
 | 
			
		||||
        component: "",
 | 
			
		||||
        modelName,
 | 
			
		||||
        converter,
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const providerModelsList = _providerModelsTable.map(mapProviders);
 | 
			
		||||
 | 
			
		||||
export const providerRendererList = new Map<string, ProviderRenderer>(
 | 
			
		||||
    _providerModelsTable.map(([modelName, _0, _1, renderer]) => [modelName, renderer]),
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export default providerModelsList;
 | 
			
		||||
@ -0,0 +1,68 @@
 | 
			
		||||
import "@goauthentik/components/ak-radio-input";
 | 
			
		||||
import "@goauthentik/components/ak-switch-input";
 | 
			
		||||
import "@goauthentik/components/ak-text-input";
 | 
			
		||||
import "@goauthentik/elements/forms/FormGroup";
 | 
			
		||||
import "@goauthentik/elements/forms/FormGroup";
 | 
			
		||||
import "@goauthentik/elements/forms/HorizontalFormElement";
 | 
			
		||||
 | 
			
		||||
import { msg } from "@lit/localize";
 | 
			
		||||
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
 | 
			
		||||
import { html } from "lit";
 | 
			
		||||
import { map } from "lit/directives/map.js";
 | 
			
		||||
 | 
			
		||||
import BasePanel from "../BasePanel";
 | 
			
		||||
import providerModelsList from "./ak-application-wizard-authentication-method-choice.choices";
 | 
			
		||||
import type { LocalTypeCreate } from "./ak-application-wizard-authentication-method-choice.choices";
 | 
			
		||||
 | 
			
		||||
@customElement("ak-application-wizard-authentication-method-choice")
 | 
			
		||||
export class ApplicationWizardAuthenticationMethodChoice extends BasePanel {
 | 
			
		||||
    constructor() {
 | 
			
		||||
        super();
 | 
			
		||||
        this.handleChoice = this.handleChoice.bind(this);
 | 
			
		||||
        this.renderProvider = this.renderProvider.bind(this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    handleChoice(ev: InputEvent) {
 | 
			
		||||
        const target = ev.target as HTMLInputElement;
 | 
			
		||||
        this.dispatchWizardUpdate({
 | 
			
		||||
            update: { providerModel: target.value },
 | 
			
		||||
            status: this.validator() ? "valid" : "invalid",
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    validator() {
 | 
			
		||||
        const radios = Array.from(this.form.querySelectorAll('input[type="radio"]'));
 | 
			
		||||
        const chosen = radios.find(
 | 
			
		||||
            (radio: Element) => radio instanceof HTMLInputElement && radio.checked,
 | 
			
		||||
        );
 | 
			
		||||
        return !!chosen;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    renderProvider(type: LocalTypeCreate) {
 | 
			
		||||
        const method = this.wizard.providerModel;
 | 
			
		||||
 | 
			
		||||
        return html`<div class="pf-c-radio">
 | 
			
		||||
            <input
 | 
			
		||||
                class="pf-c-radio__input"
 | 
			
		||||
                type="radio"
 | 
			
		||||
                name="type"
 | 
			
		||||
                id="provider-${type.formName}"
 | 
			
		||||
                value=${type.formName}
 | 
			
		||||
                ?checked=${type.formName === method}
 | 
			
		||||
                @change=${this.handleChoice}
 | 
			
		||||
            />
 | 
			
		||||
            <label class="pf-c-radio__label" for="provider-${type.formName}">${type.name}</label>
 | 
			
		||||
            <span class="pf-c-radio__description">${type.description}</span>
 | 
			
		||||
        </div>`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render() {
 | 
			
		||||
        return providerModelsList.length > 0
 | 
			
		||||
            ? html`<form class="pf-c-form pf-m-horizontal">
 | 
			
		||||
                  ${map(providerModelsList, this.renderProvider)}
 | 
			
		||||
              </form>`
 | 
			
		||||
            : html`<ak-empty-state loading header=${msg("Loading")}></ak-empty-state>`;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default ApplicationWizardAuthenticationMethodChoice;
 | 
			
		||||
@ -0,0 +1,202 @@
 | 
			
		||||
import { EVENT_REFRESH } from "@goauthentik/app/common/constants";
 | 
			
		||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
 | 
			
		||||
import "@goauthentik/components/ak-radio-input";
 | 
			
		||||
import "@goauthentik/components/ak-switch-input";
 | 
			
		||||
import "@goauthentik/components/ak-text-input";
 | 
			
		||||
import "@goauthentik/elements/forms/FormGroup";
 | 
			
		||||
import "@goauthentik/elements/forms/FormGroup";
 | 
			
		||||
import "@goauthentik/elements/forms/HorizontalFormElement";
 | 
			
		||||
 | 
			
		||||
import { msg } from "@lit/localize";
 | 
			
		||||
import { customElement, state } from "@lit/reactive-element/decorators.js";
 | 
			
		||||
import { TemplateResult, css, html, nothing } from "lit";
 | 
			
		||||
import { classMap } from "lit/directives/class-map.js";
 | 
			
		||||
 | 
			
		||||
import PFEmptyState from "@patternfly/patternfly/components/EmptyState/empty-state.css";
 | 
			
		||||
import PFProgressStepper from "@patternfly/patternfly/components/ProgressStepper/progress-stepper.css";
 | 
			
		||||
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
 | 
			
		||||
import PFBullseye from "@patternfly/patternfly/layouts/Bullseye/bullseye.css";
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
    ApplicationRequest,
 | 
			
		||||
    CoreApi,
 | 
			
		||||
    TransactionApplicationRequest,
 | 
			
		||||
    TransactionApplicationResponse,
 | 
			
		||||
} from "@goauthentik/api";
 | 
			
		||||
import type { ModelRequest } from "@goauthentik/api";
 | 
			
		||||
 | 
			
		||||
import BasePanel from "../BasePanel";
 | 
			
		||||
import providerModelsList from "../auth-method-choice/ak-application-wizard-authentication-method-choice.choices";
 | 
			
		||||
 | 
			
		||||
function cleanApplication(app: Partial<ApplicationRequest>): ApplicationRequest {
 | 
			
		||||
    return {
 | 
			
		||||
        name: "",
 | 
			
		||||
        slug: "",
 | 
			
		||||
        ...app,
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ProviderModelType = Exclude<ModelRequest["providerModel"], "11184809">;
 | 
			
		||||
 | 
			
		||||
type State = {
 | 
			
		||||
    state: "idle" | "running" | "error" | "success";
 | 
			
		||||
    label: string | TemplateResult;
 | 
			
		||||
    icon: string[];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const idleState: State = {
 | 
			
		||||
    state: "idle",
 | 
			
		||||
    label: "",
 | 
			
		||||
    icon: ["fa-cogs", "pf-m-pending"],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const runningState: State = {
 | 
			
		||||
    state: "running",
 | 
			
		||||
    label: msg("Saving Application..."),
 | 
			
		||||
    icon: ["fa-cogs", "pf-m-info"],
 | 
			
		||||
};
 | 
			
		||||
const errorState: State = {
 | 
			
		||||
    state: "error",
 | 
			
		||||
    label: msg("Authentik was unable to save this application:"),
 | 
			
		||||
    icon: ["fa-times-circle", "pf-m-danger"],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const successState: State = {
 | 
			
		||||
    state: "success",
 | 
			
		||||
    label: msg("Your application has been saved"),
 | 
			
		||||
    icon: ["fa-check-circle", "pf-m-success"],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@customElement("ak-application-wizard-commit-application")
 | 
			
		||||
export class ApplicationWizardCommitApplication extends BasePanel {
 | 
			
		||||
    static get styles() {
 | 
			
		||||
        return [
 | 
			
		||||
            ...super.styles,
 | 
			
		||||
            PFBullseye,
 | 
			
		||||
            PFEmptyState,
 | 
			
		||||
            PFTitle,
 | 
			
		||||
            PFProgressStepper,
 | 
			
		||||
            css`
 | 
			
		||||
                .pf-c-title {
 | 
			
		||||
                    padding-bottom: var(--pf-global--spacer--md);
 | 
			
		||||
                }
 | 
			
		||||
            `,
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @state()
 | 
			
		||||
    commitState: State = idleState;
 | 
			
		||||
 | 
			
		||||
    @state()
 | 
			
		||||
    errors: string[] = [];
 | 
			
		||||
 | 
			
		||||
    response?: TransactionApplicationResponse;
 | 
			
		||||
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | 
			
		||||
    willUpdate(_changedProperties: Map<string, any>) {
 | 
			
		||||
        if (this.commitState === idleState) {
 | 
			
		||||
            this.response = undefined;
 | 
			
		||||
            this.commitState = runningState;
 | 
			
		||||
            const providerModel = providerModelsList.find(
 | 
			
		||||
                ({ formName }) => formName === this.wizard.providerModel,
 | 
			
		||||
            );
 | 
			
		||||
            if (!providerModel) {
 | 
			
		||||
                throw new Error(
 | 
			
		||||
                    `Could not determine provider model from user request: ${JSON.stringify(
 | 
			
		||||
                        this.wizard,
 | 
			
		||||
                        null,
 | 
			
		||||
                        2,
 | 
			
		||||
                    )}`,
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const request: TransactionApplicationRequest = {
 | 
			
		||||
                providerModel: providerModel.modelName as ProviderModelType,
 | 
			
		||||
                app: cleanApplication(this.wizard.app),
 | 
			
		||||
                provider: providerModel.converter(this.wizard.provider),
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            this.send(request);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | 
			
		||||
    decodeErrors(body: Record<string, any>) {
 | 
			
		||||
        const spaceify = (src: Record<string, string>) =>
 | 
			
		||||
            Object.values(src).map((msg) => `\u00a0\u00a0\u00a0\u00a0${msg}`);
 | 
			
		||||
 | 
			
		||||
        let errs: string[] = [];
 | 
			
		||||
        if (body["app"] !== undefined) {
 | 
			
		||||
            errs = [...errs, msg("In the Application:"), ...spaceify(body["app"])];
 | 
			
		||||
        }
 | 
			
		||||
        if (body["provider"] !== undefined) {
 | 
			
		||||
            errs = [...errs, msg("In the Provider:"), ...spaceify(body["provider"])];
 | 
			
		||||
        }
 | 
			
		||||
        console.log(body, errs);
 | 
			
		||||
        return errs;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async send(
 | 
			
		||||
        data: TransactionApplicationRequest,
 | 
			
		||||
    ): Promise<TransactionApplicationResponse | void> {
 | 
			
		||||
        this.errors = [];
 | 
			
		||||
 | 
			
		||||
        new CoreApi(DEFAULT_CONFIG)
 | 
			
		||||
            .coreTransactionalApplicationsUpdate({
 | 
			
		||||
                transactionApplicationRequest: data,
 | 
			
		||||
            })
 | 
			
		||||
            .then((response: TransactionApplicationResponse) => {
 | 
			
		||||
                this.response = response;
 | 
			
		||||
                this.dispatchCustomEvent(EVENT_REFRESH);
 | 
			
		||||
                this.dispatchWizardUpdate({ status: "submitted" });
 | 
			
		||||
                this.commitState = successState;
 | 
			
		||||
            })
 | 
			
		||||
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | 
			
		||||
            .catch((resolution: any) => {
 | 
			
		||||
                resolution.response.json().then(
 | 
			
		||||
                    // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | 
			
		||||
                    (body: Record<string, any>) => {
 | 
			
		||||
                        this.errors = this.decodeErrors(body);
 | 
			
		||||
                    },
 | 
			
		||||
                );
 | 
			
		||||
                this.commitState = errorState;
 | 
			
		||||
            });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render(): TemplateResult {
 | 
			
		||||
        const icon = classMap(
 | 
			
		||||
            this.commitState.icon.reduce((acc, icon) => ({ ...acc, [icon]: true }), {}),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        return html`
 | 
			
		||||
            <div>
 | 
			
		||||
                <div class="pf-l-bullseye">
 | 
			
		||||
                    <div class="pf-c-empty-state pf-m-lg">
 | 
			
		||||
                        <div class="pf-c-empty-state__content">
 | 
			
		||||
                            <i
 | 
			
		||||
                                class="fas fa- ${icon} pf-c-empty-state__icon"
 | 
			
		||||
                                aria-hidden="true"
 | 
			
		||||
                            ></i>
 | 
			
		||||
                            <h1
 | 
			
		||||
                                data-commit-state=${this.commitState.state}
 | 
			
		||||
                                class="pf-c-title pf-m-lg"
 | 
			
		||||
                            >
 | 
			
		||||
                                ${this.commitState.label}
 | 
			
		||||
                            </h1>
 | 
			
		||||
                            ${this.errors.length > 0
 | 
			
		||||
                                ? html`<ul>
 | 
			
		||||
                                      ${this.errors.map(
 | 
			
		||||
                                          (msg) => html`<li><code>${msg}</code></li>`,
 | 
			
		||||
                                      )}
 | 
			
		||||
                                  </ul>`
 | 
			
		||||
                                : nothing}
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        `;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default ApplicationWizardCommitApplication;
 | 
			
		||||
@ -1,73 +0,0 @@
 | 
			
		||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
 | 
			
		||||
import { KeyUnknown } from "@goauthentik/elements/forms/Form";
 | 
			
		||||
import "@goauthentik/elements/forms/HorizontalFormElement";
 | 
			
		||||
import { WizardFormPage } from "@goauthentik/elements/wizard/WizardFormPage";
 | 
			
		||||
 | 
			
		||||
import { msg } from "@lit/localize";
 | 
			
		||||
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
 | 
			
		||||
import { TemplateResult, html } from "lit";
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
    CoreApi,
 | 
			
		||||
    FlowDesignationEnum,
 | 
			
		||||
    FlowsApi,
 | 
			
		||||
    LDAPProviderRequest,
 | 
			
		||||
    ProvidersApi,
 | 
			
		||||
    UserServiceAccountResponse,
 | 
			
		||||
} from "@goauthentik/api";
 | 
			
		||||
 | 
			
		||||
@customElement("ak-application-wizard-type-ldap")
 | 
			
		||||
export class TypeLDAPApplicationWizardPage extends WizardFormPage {
 | 
			
		||||
    sidebarLabel = () => msg("LDAP details");
 | 
			
		||||
 | 
			
		||||
    nextDataCallback = async (data: KeyUnknown): Promise<boolean> => {
 | 
			
		||||
        let name = this.host.state["name"] as string;
 | 
			
		||||
        // Check if a provider with the name already exists
 | 
			
		||||
        const providers = await new ProvidersApi(DEFAULT_CONFIG).providersAllList({
 | 
			
		||||
            search: name,
 | 
			
		||||
        });
 | 
			
		||||
        if (providers.results.filter((provider) => provider.name == name)) {
 | 
			
		||||
            name += "-1";
 | 
			
		||||
        }
 | 
			
		||||
        this.host.addActionBefore(msg("Create service account"), async (): Promise<boolean> => {
 | 
			
		||||
            const serviceAccount = await new CoreApi(DEFAULT_CONFIG).coreUsersServiceAccountCreate({
 | 
			
		||||
                userServiceAccountRequest: {
 | 
			
		||||
                    name: name,
 | 
			
		||||
                    createGroup: true,
 | 
			
		||||
                },
 | 
			
		||||
            });
 | 
			
		||||
            this.host.state["serviceAccount"] = serviceAccount;
 | 
			
		||||
            return true;
 | 
			
		||||
        });
 | 
			
		||||
        this.host.addActionBefore(msg("Create provider"), async (): Promise<boolean> => {
 | 
			
		||||
            // Get all flows and default to the implicit authorization
 | 
			
		||||
            const flows = await new FlowsApi(DEFAULT_CONFIG).flowsInstancesList({
 | 
			
		||||
                designation: FlowDesignationEnum.Authorization,
 | 
			
		||||
                ordering: "slug",
 | 
			
		||||
            });
 | 
			
		||||
            const serviceAccount = this.host.state["serviceAccount"] as UserServiceAccountResponse;
 | 
			
		||||
            const req: LDAPProviderRequest = {
 | 
			
		||||
                name: name,
 | 
			
		||||
                authorizationFlow: flows.results[0].pk,
 | 
			
		||||
                baseDn: data.baseDN as string,
 | 
			
		||||
                searchGroup: serviceAccount.groupPk,
 | 
			
		||||
            };
 | 
			
		||||
            const provider = await new ProvidersApi(DEFAULT_CONFIG).providersLdapCreate({
 | 
			
		||||
                lDAPProviderRequest: req,
 | 
			
		||||
            });
 | 
			
		||||
            this.host.state["provider"] = provider;
 | 
			
		||||
            return true;
 | 
			
		||||
        });
 | 
			
		||||
        return true;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    renderForm(): TemplateResult {
 | 
			
		||||
        const domainParts = window.location.hostname.split(".");
 | 
			
		||||
        const defaultBaseDN = domainParts.map((part) => `dc=${part}`).join(",");
 | 
			
		||||
        return html`<form class="pf-c-form pf-m-horizontal">
 | 
			
		||||
            <ak-form-element-horizontal label=${msg("Base DN")} name="baseDN" ?required=${true}>
 | 
			
		||||
                <input type="text" value="${defaultBaseDN}" class="pf-c-form-control" required />
 | 
			
		||||
            </ak-form-element-horizontal>
 | 
			
		||||
        </form> `;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,30 +0,0 @@
 | 
			
		||||
import { KeyUnknown } from "@goauthentik/elements/forms/Form";
 | 
			
		||||
import "@goauthentik/elements/forms/HorizontalFormElement";
 | 
			
		||||
import { WizardFormPage } from "@goauthentik/elements/wizard/WizardFormPage";
 | 
			
		||||
 | 
			
		||||
import { msg } from "@lit/localize";
 | 
			
		||||
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
 | 
			
		||||
import { TemplateResult, html } from "lit";
 | 
			
		||||
 | 
			
		||||
@customElement("ak-application-wizard-type-link")
 | 
			
		||||
export class TypeLinkApplicationWizardPage extends WizardFormPage {
 | 
			
		||||
    sidebarLabel = () => msg("Application Link");
 | 
			
		||||
 | 
			
		||||
    nextDataCallback = async (data: KeyUnknown): Promise<boolean> => {
 | 
			
		||||
        this.host.state["link"] = data.link;
 | 
			
		||||
        return true;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    renderForm(): TemplateResult {
 | 
			
		||||
        return html`
 | 
			
		||||
            <form class="pf-c-form pf-m-horizontal">
 | 
			
		||||
                <ak-form-element-horizontal label=${msg("Link")} ?required=${true} name="link">
 | 
			
		||||
                    <input type="text" value="" class="pf-c-form-control" required />
 | 
			
		||||
                    <p class="pf-c-form__helper-text">
 | 
			
		||||
                        ${msg("URL which will be opened when a user clicks on the application.")}
 | 
			
		||||
                    </p>
 | 
			
		||||
                </ak-form-element-horizontal>
 | 
			
		||||
            </form>
 | 
			
		||||
        `;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,26 @@
 | 
			
		||||
import BasePanel from "../BasePanel";
 | 
			
		||||
 | 
			
		||||
export class ApplicationWizardProviderPageBase extends BasePanel {
 | 
			
		||||
    handleChange(ev: InputEvent) {
 | 
			
		||||
        if (!ev.target) {
 | 
			
		||||
            console.warn(`Received event with no target: ${ev}`);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        const target = ev.target as HTMLInputElement;
 | 
			
		||||
        const value = target.type === "checkbox" ? target.checked : target.value;
 | 
			
		||||
        this.dispatchWizardUpdate({
 | 
			
		||||
            update: {
 | 
			
		||||
                provider: {
 | 
			
		||||
                    [target.name]: value,
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
            status: this.form.checkValidity() ? "valid" : "invalid",
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    validator() {
 | 
			
		||||
        return this.form.reportValidity();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default ApplicationWizardProviderPageBase;
 | 
			
		||||
@ -0,0 +1,29 @@
 | 
			
		||||
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
 | 
			
		||||
 | 
			
		||||
import BasePanel from "../BasePanel";
 | 
			
		||||
import { providerRendererList } from "../auth-method-choice/ak-application-wizard-authentication-method-choice.choices";
 | 
			
		||||
import "./ldap/ak-application-wizard-authentication-by-ldap";
 | 
			
		||||
import "./oauth/ak-application-wizard-authentication-by-oauth";
 | 
			
		||||
import "./proxy/ak-application-wizard-authentication-for-forward-domain-proxy";
 | 
			
		||||
import "./proxy/ak-application-wizard-authentication-for-reverse-proxy";
 | 
			
		||||
import "./proxy/ak-application-wizard-authentication-for-single-forward-proxy";
 | 
			
		||||
import "./radius/ak-application-wizard-authentication-by-radius";
 | 
			
		||||
import "./saml/ak-application-wizard-authentication-by-saml-configuration";
 | 
			
		||||
import "./scim/ak-application-wizard-authentication-by-scim";
 | 
			
		||||
 | 
			
		||||
// prettier-ignore
 | 
			
		||||
 | 
			
		||||
@customElement("ak-application-wizard-authentication-method")
 | 
			
		||||
export class ApplicationWizardApplicationDetails extends BasePanel {
 | 
			
		||||
    render() {
 | 
			
		||||
        const handler = providerRendererList.get(this.wizard.providerModel);
 | 
			
		||||
        if (!handler) {
 | 
			
		||||
            throw new Error(
 | 
			
		||||
                "Unrecognized authentication method in ak-application-wizard-authentication-method",
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
        return handler();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default ApplicationWizardApplicationDetails;
 | 
			
		||||
@ -0,0 +1,64 @@
 | 
			
		||||
import { msg } from "@lit/localize";
 | 
			
		||||
import { html } from "lit";
 | 
			
		||||
 | 
			
		||||
import { LDAPAPIAccessMode } from "@goauthentik/api";
 | 
			
		||||
 | 
			
		||||
export const bindModeOptions = [
 | 
			
		||||
    {
 | 
			
		||||
        label: msg("Cached binding"),
 | 
			
		||||
        value: LDAPAPIAccessMode.Cached,
 | 
			
		||||
        default: true,
 | 
			
		||||
        description: html`${msg(
 | 
			
		||||
            "Flow is executed and session is cached in memory. Flow is executed when session expires",
 | 
			
		||||
        )}`,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        label: msg("Direct binding"),
 | 
			
		||||
        value: LDAPAPIAccessMode.Direct,
 | 
			
		||||
        description: html`${msg(
 | 
			
		||||
            "Always execute the configured bind flow to authenticate the user",
 | 
			
		||||
        )}`,
 | 
			
		||||
    },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
export const searchModeOptions = [
 | 
			
		||||
    {
 | 
			
		||||
        label: msg("Cached querying"),
 | 
			
		||||
        value: LDAPAPIAccessMode.Cached,
 | 
			
		||||
        default: true,
 | 
			
		||||
        description: html`${msg(
 | 
			
		||||
            "The outpost holds all users and groups in-memory and will refresh every 5 Minutes",
 | 
			
		||||
        )}`,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        label: msg("Direct querying"),
 | 
			
		||||
        value: LDAPAPIAccessMode.Direct,
 | 
			
		||||
        description: html`${msg(
 | 
			
		||||
            "Always returns the latest data, but slower than cached querying",
 | 
			
		||||
        )}`,
 | 
			
		||||
    },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
export const mfaSupportHelp = msg(
 | 
			
		||||
    "When enabled, code-based multi-factor authentication can be used by appending a semicolon and the TOTP code to the password. This should only be enabled if all users that will bind to this provider have a TOTP device configured, as otherwise a password may incorrectly be rejected if it contains a semicolon.",
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export const groupHelp = msg(
 | 
			
		||||
    "The start for gidNumbers, this number is added to a number generated from the group.Pk to make sure that the numbers aren't too low for POSIX groups. Default is 4000 to ensure that we don't collide with local groups or users primary groups gidNumber",
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export const cryptoCertificateHelp = msg(
 | 
			
		||||
    "The certificate for the above configured Base DN. As a fallback, the provider uses a self-signed certificate.",
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export const tlsServerNameHelp = msg(
 | 
			
		||||
    "DNS name for which the above configured certificate should be used. The certificate cannot be detected based on the base DN, as the SSL/TLS negotiation happens before such data is exchanged.",
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export const uidStartNumberHelp = msg(
 | 
			
		||||
    "The start for uidNumbers, this number is added to the user.Pk to make sure that the numbers aren't too low for POSIX users. Default is 2000 to ensure that we don't collide with local users uidNumber",
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export const gidStartNumberHelp = msg(
 | 
			
		||||
    "The start for gidNumbers, this number is added to a number generated from the group.Pk to make sure that the numbers aren't too low for POSIX groups. Default is 4000 to ensure that we don't collide with local groups or users primary groups gidNumber",
 | 
			
		||||
);
 | 
			
		||||
@ -0,0 +1,146 @@
 | 
			
		||||
import "@goauthentik/admin/common/ak-core-group-search";
 | 
			
		||||
import "@goauthentik/admin/common/ak-crypto-certificate-search";
 | 
			
		||||
import "@goauthentik/admin/common/ak-flow-search/ak-tenanted-flow-search";
 | 
			
		||||
import { first } from "@goauthentik/common/utils";
 | 
			
		||||
import "@goauthentik/components/ak-number-input";
 | 
			
		||||
import "@goauthentik/components/ak-radio-input";
 | 
			
		||||
import "@goauthentik/components/ak-switch-input";
 | 
			
		||||
import "@goauthentik/components/ak-text-input";
 | 
			
		||||
import { rootInterface } from "@goauthentik/elements/Base";
 | 
			
		||||
import "@goauthentik/elements/forms/FormGroup";
 | 
			
		||||
import "@goauthentik/elements/forms/HorizontalFormElement";
 | 
			
		||||
 | 
			
		||||
import { msg } from "@lit/localize";
 | 
			
		||||
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
 | 
			
		||||
import { html, nothing } from "lit";
 | 
			
		||||
import { ifDefined } from "lit/directives/if-defined.js";
 | 
			
		||||
 | 
			
		||||
import { FlowsInstancesListDesignationEnum } from "@goauthentik/api";
 | 
			
		||||
import type { LDAPProvider } from "@goauthentik/api";
 | 
			
		||||
 | 
			
		||||
import BaseProviderPanel from "../BaseProviderPanel";
 | 
			
		||||
import {
 | 
			
		||||
    bindModeOptions,
 | 
			
		||||
    cryptoCertificateHelp,
 | 
			
		||||
    gidStartNumberHelp,
 | 
			
		||||
    groupHelp,
 | 
			
		||||
    mfaSupportHelp,
 | 
			
		||||
    searchModeOptions,
 | 
			
		||||
    tlsServerNameHelp,
 | 
			
		||||
    uidStartNumberHelp,
 | 
			
		||||
} from "./LDAPOptionsAndHelp";
 | 
			
		||||
 | 
			
		||||
@customElement("ak-application-wizard-authentication-by-ldap")
 | 
			
		||||
export class ApplicationWizardApplicationDetails extends BaseProviderPanel {
 | 
			
		||||
    render() {
 | 
			
		||||
        const provider = this.wizard.provider as LDAPProvider | undefined;
 | 
			
		||||
 | 
			
		||||
        return html` <form class="pf-c-form pf-m-horizontal" @input=${this.handleChange}>
 | 
			
		||||
            <ak-text-input
 | 
			
		||||
                name="name"
 | 
			
		||||
                value=${ifDefined(provider?.name)}
 | 
			
		||||
                label=${msg("Name")}
 | 
			
		||||
                required
 | 
			
		||||
                help=${msg("Method's display Name.")}
 | 
			
		||||
            ></ak-text-input>
 | 
			
		||||
 | 
			
		||||
            <ak-form-element-horizontal
 | 
			
		||||
                label=${msg("Bind flow")}
 | 
			
		||||
                ?required=${true}
 | 
			
		||||
                name="authorizationFlow"
 | 
			
		||||
            >
 | 
			
		||||
                <ak-tenanted-flow-search
 | 
			
		||||
                    flowType=${FlowsInstancesListDesignationEnum.Authentication}
 | 
			
		||||
                    .currentFlow=${provider?.authorizationFlow}
 | 
			
		||||
                    .tenantFlow=${rootInterface()?.tenant?.flowAuthentication}
 | 
			
		||||
                    required
 | 
			
		||||
                ></ak-tenanted-flow-search>
 | 
			
		||||
                <p class="pf-c-form__helper-text">${msg("Flow used for users to authenticate.")}</p>
 | 
			
		||||
            </ak-form-element-horizontal>
 | 
			
		||||
 | 
			
		||||
            <ak-form-element-horizontal label=${msg("Search group")} name="searchGroup">
 | 
			
		||||
                <ak-core-group-search
 | 
			
		||||
                    name="searchGroup"
 | 
			
		||||
                    group=${ifDefined(provider?.searchGroup ?? nothing)}
 | 
			
		||||
                ></ak-core-group-search>
 | 
			
		||||
                <p class="pf-c-form__helper-text">${groupHelp}</p>
 | 
			
		||||
            </ak-form-element-horizontal>
 | 
			
		||||
 | 
			
		||||
            <ak-radio-input
 | 
			
		||||
                label=${msg("Bind mode")}
 | 
			
		||||
                name="bindMode"
 | 
			
		||||
                .options=${bindModeOptions}
 | 
			
		||||
                .value=${provider?.bindMode}
 | 
			
		||||
                help=${msg("Configure how the outpost authenticates requests.")}
 | 
			
		||||
            >
 | 
			
		||||
            </ak-radio-input>
 | 
			
		||||
 | 
			
		||||
            <ak-radio-input
 | 
			
		||||
                label=${msg("Search mode")}
 | 
			
		||||
                name="searchMode"
 | 
			
		||||
                .options=${searchModeOptions}
 | 
			
		||||
                .value=${provider?.searchMode}
 | 
			
		||||
                help=${msg("Configure how the outpost queries the core authentik server's users.")}
 | 
			
		||||
            >
 | 
			
		||||
            </ak-radio-input>
 | 
			
		||||
 | 
			
		||||
            <ak-switch-input
 | 
			
		||||
                name="openInNewTab"
 | 
			
		||||
                label=${msg("Code-based MFA Support")}
 | 
			
		||||
                ?checked=${provider?.mfaSupport}
 | 
			
		||||
                help=${mfaSupportHelp}
 | 
			
		||||
            >
 | 
			
		||||
            </ak-switch-input>
 | 
			
		||||
 | 
			
		||||
            <ak-form-group .expanded=${true}>
 | 
			
		||||
                <span slot="header"> ${msg("Protocol settings")} </span>
 | 
			
		||||
                <div slot="body" class="pf-c-form">
 | 
			
		||||
                    <ak-text-input
 | 
			
		||||
                        name="baseDn"
 | 
			
		||||
                        label=${msg("Base DN")}
 | 
			
		||||
                        required
 | 
			
		||||
                        value="${first(provider?.baseDn, "DC=ldap,DC=goauthentik,DC=io")}"
 | 
			
		||||
                        help=${msg(
 | 
			
		||||
                            "LDAP DN under which bind requests and search requests can be made.",
 | 
			
		||||
                        )}
 | 
			
		||||
                    >
 | 
			
		||||
                    </ak-text-input>
 | 
			
		||||
 | 
			
		||||
                    <ak-form-element-horizontal label=${msg("Certificate")} name="certificate">
 | 
			
		||||
                        <ak-crypto-certificate-search
 | 
			
		||||
                            certificate=${ifDefined(provider?.certificate ?? nothing)}
 | 
			
		||||
                            name="certificate"
 | 
			
		||||
                        >
 | 
			
		||||
                        </ak-crypto-certificate-search>
 | 
			
		||||
                        <p class="pf-c-form__helper-text">${cryptoCertificateHelp}</p>
 | 
			
		||||
                    </ak-form-element-horizontal>
 | 
			
		||||
 | 
			
		||||
                    <ak-text-input
 | 
			
		||||
                        label=${msg("TLS Server name")}
 | 
			
		||||
                        name="tlsServerName"
 | 
			
		||||
                        value="${first(provider?.tlsServerName, "")}"
 | 
			
		||||
                        help=${tlsServerNameHelp}
 | 
			
		||||
                    ></ak-text-input>
 | 
			
		||||
 | 
			
		||||
                    <ak-number-input
 | 
			
		||||
                        label=${msg("UID start number")}
 | 
			
		||||
                        required
 | 
			
		||||
                        name="uidStartNumber"
 | 
			
		||||
                        value="${first(provider?.uidStartNumber, 2000)}"
 | 
			
		||||
                        help=${uidStartNumberHelp}
 | 
			
		||||
                    ></ak-number-input>
 | 
			
		||||
 | 
			
		||||
                    <ak-number-input
 | 
			
		||||
                        label=${msg("GID start number")}
 | 
			
		||||
                        required
 | 
			
		||||
                        name="gidStartNumber"
 | 
			
		||||
                        value="${first(provider?.gidStartNumber, 4000)}"
 | 
			
		||||
                        help=${gidStartNumberHelp}
 | 
			
		||||
                    ></ak-number-input>
 | 
			
		||||
                </div>
 | 
			
		||||
            </ak-form-group>
 | 
			
		||||
        </form>`;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default ApplicationWizardApplicationDetails;
 | 
			
		||||
@ -0,0 +1,301 @@
 | 
			
		||||
import "@goauthentik/admin/common/ak-crypto-certificate-search";
 | 
			
		||||
import "@goauthentik/admin/common/ak-flow-search/ak-tenanted-flow-search";
 | 
			
		||||
import {
 | 
			
		||||
    clientTypeOptions,
 | 
			
		||||
    issuerModeOptions,
 | 
			
		||||
    redirectUriHelp,
 | 
			
		||||
    subjectModeOptions,
 | 
			
		||||
} from "@goauthentik/admin/providers/oauth2/OAuth2ProviderForm";
 | 
			
		||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
 | 
			
		||||
import { ascii_letters, digits, first, randomString } from "@goauthentik/common/utils";
 | 
			
		||||
import "@goauthentik/components/ak-number-input";
 | 
			
		||||
import "@goauthentik/components/ak-radio-input";
 | 
			
		||||
import "@goauthentik/components/ak-switch-input";
 | 
			
		||||
import "@goauthentik/components/ak-text-input";
 | 
			
		||||
import "@goauthentik/components/ak-textarea-input";
 | 
			
		||||
import "@goauthentik/elements/forms/FormGroup";
 | 
			
		||||
import "@goauthentik/elements/forms/HorizontalFormElement";
 | 
			
		||||
 | 
			
		||||
import { msg } from "@lit/localize";
 | 
			
		||||
import { customElement, state } from "@lit/reactive-element/decorators.js";
 | 
			
		||||
import { html, nothing } from "lit";
 | 
			
		||||
import { ifDefined } from "lit/directives/if-defined.js";
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
    ClientTypeEnum,
 | 
			
		||||
    FlowsInstancesListDesignationEnum,
 | 
			
		||||
    PropertymappingsApi,
 | 
			
		||||
    SourcesApi,
 | 
			
		||||
} from "@goauthentik/api";
 | 
			
		||||
import type {
 | 
			
		||||
    OAuth2Provider,
 | 
			
		||||
    PaginatedOAuthSourceList,
 | 
			
		||||
    PaginatedScopeMappingList,
 | 
			
		||||
} from "@goauthentik/api";
 | 
			
		||||
 | 
			
		||||
import BaseProviderPanel from "../BaseProviderPanel";
 | 
			
		||||
 | 
			
		||||
@customElement("ak-application-wizard-authentication-by-oauth")
 | 
			
		||||
export class ApplicationWizardAuthenticationByOauth extends BaseProviderPanel {
 | 
			
		||||
    @state()
 | 
			
		||||
    showClientSecret = false;
 | 
			
		||||
 | 
			
		||||
    @state()
 | 
			
		||||
    propertyMappings?: PaginatedScopeMappingList;
 | 
			
		||||
 | 
			
		||||
    @state()
 | 
			
		||||
    oauthSources?: PaginatedOAuthSourceList;
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        super();
 | 
			
		||||
        new PropertymappingsApi(DEFAULT_CONFIG)
 | 
			
		||||
            .propertymappingsScopeList({
 | 
			
		||||
                ordering: "scope_name",
 | 
			
		||||
            })
 | 
			
		||||
            .then((propertyMappings: PaginatedScopeMappingList) => {
 | 
			
		||||
                this.propertyMappings = propertyMappings;
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
        new SourcesApi(DEFAULT_CONFIG)
 | 
			
		||||
            .sourcesOauthList({
 | 
			
		||||
                ordering: "name",
 | 
			
		||||
                hasJwks: true,
 | 
			
		||||
            })
 | 
			
		||||
            .then((oauthSources: PaginatedOAuthSourceList) => {
 | 
			
		||||
                this.oauthSources = oauthSources;
 | 
			
		||||
            });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render() {
 | 
			
		||||
        const provider = this.wizard.provider as OAuth2Provider | undefined;
 | 
			
		||||
 | 
			
		||||
        return html`<form class="pf-c-form pf-m-horizontal" @input=${this.handleChange}>
 | 
			
		||||
            <ak-text-input
 | 
			
		||||
                name="name"
 | 
			
		||||
                label=${msg("Name")}
 | 
			
		||||
                value=${ifDefined(provider?.name)}
 | 
			
		||||
                required
 | 
			
		||||
            ></ak-text-input>
 | 
			
		||||
 | 
			
		||||
            <ak-form-element-horizontal
 | 
			
		||||
                name="authenticationFlow"
 | 
			
		||||
                label=${msg("Authentication flow")}
 | 
			
		||||
            >
 | 
			
		||||
                <ak-flow-search
 | 
			
		||||
                    flowType=${FlowsInstancesListDesignationEnum.Authentication}
 | 
			
		||||
                    .currentFlow=${provider?.authenticationFlow}
 | 
			
		||||
                    required
 | 
			
		||||
                ></ak-flow-search>
 | 
			
		||||
                <p class="pf-c-form__helper-text">
 | 
			
		||||
                    ${msg("Flow used when a user access this provider and is not authenticated.")}
 | 
			
		||||
                </p>
 | 
			
		||||
            </ak-form-element-horizontal>
 | 
			
		||||
 | 
			
		||||
            <ak-form-element-horizontal
 | 
			
		||||
                name="authorizationFlow"
 | 
			
		||||
                label=${msg("Authorization flow")}
 | 
			
		||||
                ?required=${true}
 | 
			
		||||
            >
 | 
			
		||||
                <ak-flow-search
 | 
			
		||||
                    flowType=${FlowsInstancesListDesignationEnum.Authorization}
 | 
			
		||||
                    .currentFlow=${provider?.authorizationFlow}
 | 
			
		||||
                    required
 | 
			
		||||
                ></ak-flow-search>
 | 
			
		||||
                <p class="pf-c-form__helper-text">
 | 
			
		||||
                    ${msg("Flow used when authorizing this provider.")}
 | 
			
		||||
                </p>
 | 
			
		||||
            </ak-form-element-horizontal>
 | 
			
		||||
 | 
			
		||||
            <ak-form-group .expanded=${true}>
 | 
			
		||||
                <span slot="header"> ${msg("Protocol settings")} </span>
 | 
			
		||||
                <div slot="body" class="pf-c-form">
 | 
			
		||||
                    <ak-radio-input
 | 
			
		||||
                        name="clientType"
 | 
			
		||||
                        label=${msg("Client type")}
 | 
			
		||||
                        .value=${provider?.clientType}
 | 
			
		||||
                        required
 | 
			
		||||
                        @change=${(ev: CustomEvent<ClientTypeEnum>) => {
 | 
			
		||||
                            this.showClientSecret = ev.detail !== ClientTypeEnum.Public;
 | 
			
		||||
                        }}
 | 
			
		||||
                        .options=${clientTypeOptions}
 | 
			
		||||
                    >
 | 
			
		||||
                    </ak-radio-input>
 | 
			
		||||
 | 
			
		||||
                    <ak-text-input
 | 
			
		||||
                        name="clientId"
 | 
			
		||||
                        label=${msg("Client ID")}
 | 
			
		||||
                        value="${first(
 | 
			
		||||
                            provider?.clientId,
 | 
			
		||||
                            randomString(40, ascii_letters + digits),
 | 
			
		||||
                        )}"
 | 
			
		||||
                        required
 | 
			
		||||
                    >
 | 
			
		||||
                    </ak-text-input>
 | 
			
		||||
 | 
			
		||||
                    <ak-text-input
 | 
			
		||||
                        name="clientSecret"
 | 
			
		||||
                        label=${msg("Client Secret")}
 | 
			
		||||
                        value="${first(
 | 
			
		||||
                            provider?.clientSecret,
 | 
			
		||||
                            randomString(128, ascii_letters + digits),
 | 
			
		||||
                        )}"
 | 
			
		||||
                        ?hidden=${!this.showClientSecret}
 | 
			
		||||
                    >
 | 
			
		||||
                    </ak-text-input>
 | 
			
		||||
 | 
			
		||||
                    <ak-textarea-input
 | 
			
		||||
                        name="redirectUris"
 | 
			
		||||
                        label=${msg("Redirect URIs/Origins (RegEx)")}
 | 
			
		||||
                        .value=${provider?.redirectUris}
 | 
			
		||||
                        .bighelp=${redirectUriHelp}
 | 
			
		||||
                    >
 | 
			
		||||
                    </ak-textarea-input>
 | 
			
		||||
 | 
			
		||||
                    <ak-form-element-horizontal label=${msg("Signing Key")} name="signingKey">
 | 
			
		||||
                        <ak-crypto-certificate-search
 | 
			
		||||
                            certificate=${ifDefined(provider?.signingKey ?? nothing)}
 | 
			
		||||
                            name="certificate"
 | 
			
		||||
                            singleton
 | 
			
		||||
                        >
 | 
			
		||||
                        </ak-crypto-certificate-search>
 | 
			
		||||
                        <p class="pf-c-form__helper-text">${msg("Key used to sign the tokens.")}</p>
 | 
			
		||||
                    </ak-form-element-horizontal>
 | 
			
		||||
                </div>
 | 
			
		||||
            </ak-form-group>
 | 
			
		||||
 | 
			
		||||
            <ak-form-group>
 | 
			
		||||
                <span slot="header"> ${msg("Advanced protocol settings")} </span>
 | 
			
		||||
                <div slot="body" class="pf-c-form">
 | 
			
		||||
                    <ak-text-input
 | 
			
		||||
                        name="accessCodeValidity"
 | 
			
		||||
                        label=${msg("Access code validity")}
 | 
			
		||||
                        required
 | 
			
		||||
                        value="${first(provider?.accessCodeValidity, "minutes=1")}"
 | 
			
		||||
                        .bighelp=${html`<p class="pf-c-form__helper-text">
 | 
			
		||||
                                ${msg("Configure how long access codes are valid for.")}
 | 
			
		||||
                            </p>
 | 
			
		||||
                            <ak-utils-time-delta-help></ak-utils-time-delta-help>`}
 | 
			
		||||
                    >
 | 
			
		||||
                    </ak-text-input>
 | 
			
		||||
 | 
			
		||||
                    <ak-text-input
 | 
			
		||||
                        name="accessTokenValidity"
 | 
			
		||||
                        label=${msg("Access Token validity")}
 | 
			
		||||
                        value="${first(provider?.accessTokenValidity, "minutes=5")}"
 | 
			
		||||
                        required
 | 
			
		||||
                        .bighelp=${html` <p class="pf-c-form__helper-text">
 | 
			
		||||
                                ${msg("Configure how long access tokens are valid for.")}
 | 
			
		||||
                            </p>
 | 
			
		||||
                            <ak-utils-time-delta-help></ak-utils-time-delta-help>`}
 | 
			
		||||
                    >
 | 
			
		||||
                    </ak-text-input>
 | 
			
		||||
 | 
			
		||||
                    <ak-text-input
 | 
			
		||||
                        name="refreshTokenValidity"
 | 
			
		||||
                        label=${msg("Refresh Token validity")}
 | 
			
		||||
                        value="${first(provider?.refreshTokenValidity, "days=30")}"
 | 
			
		||||
                        ?required=${true}
 | 
			
		||||
                        .bighelp=${html` <p class="pf-c-form__helper-text">
 | 
			
		||||
                                ${msg("Configure how long refresh tokens are valid for.")}
 | 
			
		||||
                            </p>
 | 
			
		||||
                            <ak-utils-time-delta-help></ak-utils-time-delta-help>`}
 | 
			
		||||
                    >
 | 
			
		||||
                    </ak-text-input>
 | 
			
		||||
 | 
			
		||||
                    <ak-form-element-horizontal label=${msg("Scopes")} name="propertyMappings">
 | 
			
		||||
                        <select class="pf-c-form-control" multiple>
 | 
			
		||||
                            ${this.propertyMappings?.results.map((scope) => {
 | 
			
		||||
                                let selected = false;
 | 
			
		||||
                                if (!provider?.propertyMappings) {
 | 
			
		||||
                                    selected =
 | 
			
		||||
                                        scope.managed?.startsWith(
 | 
			
		||||
                                            "goauthentik.io/providers/oauth2/scope-",
 | 
			
		||||
                                        ) || false;
 | 
			
		||||
                                } else {
 | 
			
		||||
                                    selected = Array.from(provider?.propertyMappings).some((su) => {
 | 
			
		||||
                                        return su == scope.pk;
 | 
			
		||||
                                    });
 | 
			
		||||
                                }
 | 
			
		||||
                                return html`<option
 | 
			
		||||
                                    value=${ifDefined(scope.pk)}
 | 
			
		||||
                                    ?selected=${selected}
 | 
			
		||||
                                >
 | 
			
		||||
                                    ${scope.name}
 | 
			
		||||
                                </option>`;
 | 
			
		||||
                            })}
 | 
			
		||||
                        </select>
 | 
			
		||||
                        <p class="pf-c-form__helper-text">
 | 
			
		||||
                            ${msg(
 | 
			
		||||
                                "Select which scopes can be used by the client. The client still has to specify the scope to access the data.",
 | 
			
		||||
                            )}
 | 
			
		||||
                        </p>
 | 
			
		||||
                        <p class="pf-c-form__helper-text">
 | 
			
		||||
                            ${msg("Hold control/command to select multiple items.")}
 | 
			
		||||
                        </p>
 | 
			
		||||
                    </ak-form-element-horizontal>
 | 
			
		||||
 | 
			
		||||
                    <ak-radio-input
 | 
			
		||||
                        name="subMode"
 | 
			
		||||
                        label=${msg("Subject mode")}
 | 
			
		||||
                        required
 | 
			
		||||
                        .options=${subjectModeOptions}
 | 
			
		||||
                        .value=${provider?.subMode}
 | 
			
		||||
                        help=${msg(
 | 
			
		||||
                            "Configure what data should be used as unique User Identifier. For most cases, the default should be fine.",
 | 
			
		||||
                        )}
 | 
			
		||||
                    >
 | 
			
		||||
                    </ak-radio-input>
 | 
			
		||||
                    <ak-switch-input name="includeClaimsInIdToken">
 | 
			
		||||
                        label=${msg("Include claims in id_token")}
 | 
			
		||||
                        ?checked=${first(provider?.includeClaimsInIdToken, true)}
 | 
			
		||||
                        help=${msg(
 | 
			
		||||
                            "Include User claims from scopes in the id_token, for applications that don't access the userinfo endpoint.",
 | 
			
		||||
                        )}></ak-switch-input
 | 
			
		||||
                    >
 | 
			
		||||
                    <ak-radio-input
 | 
			
		||||
                        name="issuerMode"
 | 
			
		||||
                        label=${msg("Issuer mode")}
 | 
			
		||||
                        required
 | 
			
		||||
                        .options=${issuerModeOptions}
 | 
			
		||||
                        .value=${provider?.issuerMode}
 | 
			
		||||
                        help=${msg(
 | 
			
		||||
                            "Configure how the issuer field of the ID Token should be filled.",
 | 
			
		||||
                        )}
 | 
			
		||||
                    >
 | 
			
		||||
                    </ak-radio-input>
 | 
			
		||||
                </div>
 | 
			
		||||
            </ak-form-group>
 | 
			
		||||
 | 
			
		||||
            <ak-form-group>
 | 
			
		||||
                <span slot="header">${msg("Machine-to-Machine authentication settings")}</span>
 | 
			
		||||
                <div slot="body" class="pf-c-form">
 | 
			
		||||
                    <ak-form-element-horizontal
 | 
			
		||||
                        label=${msg("Trusted OIDC Sources")}
 | 
			
		||||
                        name="jwksSources"
 | 
			
		||||
                    >
 | 
			
		||||
                        <select class="pf-c-form-control" multiple>
 | 
			
		||||
                            ${this.oauthSources?.results.map((source) => {
 | 
			
		||||
                                const selected = (provider?.jwksSources || []).some((su) => {
 | 
			
		||||
                                    return su == source.pk;
 | 
			
		||||
                                });
 | 
			
		||||
                                return html`<option value=${source.pk} ?selected=${selected}>
 | 
			
		||||
                                    ${source.name} (${source.slug})
 | 
			
		||||
                                </option>`;
 | 
			
		||||
                            })}
 | 
			
		||||
                        </select>
 | 
			
		||||
                        <p class="pf-c-form__helper-text">
 | 
			
		||||
                            ${msg(
 | 
			
		||||
                                "JWTs signed by certificates configured in the selected sources can be used to authenticate to this provider.",
 | 
			
		||||
                            )}
 | 
			
		||||
                        </p>
 | 
			
		||||
                        <p class="pf-c-form__helper-text">
 | 
			
		||||
                            ${msg("Hold control/command to select multiple items.")}
 | 
			
		||||
                        </p>
 | 
			
		||||
                    </ak-form-element-horizontal>
 | 
			
		||||
                </div>
 | 
			
		||||
            </ak-form-group>
 | 
			
		||||
        </form>`;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default ApplicationWizardAuthenticationByOauth;
 | 
			
		||||
@ -0,0 +1,255 @@
 | 
			
		||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
 | 
			
		||||
import { first } from "@goauthentik/common/utils";
 | 
			
		||||
import "@goauthentik/components/ak-switch-input";
 | 
			
		||||
import "@goauthentik/components/ak-text-input";
 | 
			
		||||
import "@goauthentik/components/ak-textarea-input";
 | 
			
		||||
import "@goauthentik/components/ak-toggle-group";
 | 
			
		||||
import "@goauthentik/elements/forms/HorizontalFormElement";
 | 
			
		||||
 | 
			
		||||
import { msg } from "@lit/localize";
 | 
			
		||||
import { state } from "@lit/reactive-element/decorators.js";
 | 
			
		||||
import { TemplateResult, html, nothing } from "lit";
 | 
			
		||||
import { ifDefined } from "lit/directives/if-defined.js";
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
    FlowsInstancesListDesignationEnum,
 | 
			
		||||
    PaginatedOAuthSourceList,
 | 
			
		||||
    PaginatedScopeMappingList,
 | 
			
		||||
    PropertymappingsApi,
 | 
			
		||||
    ProxyMode,
 | 
			
		||||
    ProxyProvider,
 | 
			
		||||
    SourcesApi,
 | 
			
		||||
} from "@goauthentik/api";
 | 
			
		||||
 | 
			
		||||
import BaseProviderPanel from "../BaseProviderPanel";
 | 
			
		||||
 | 
			
		||||
type MaybeTemplateResult = TemplateResult | typeof nothing;
 | 
			
		||||
 | 
			
		||||
export class AkTypeProxyApplicationWizardPage extends BaseProviderPanel {
 | 
			
		||||
    constructor() {
 | 
			
		||||
        super();
 | 
			
		||||
        new PropertymappingsApi(DEFAULT_CONFIG)
 | 
			
		||||
            .propertymappingsScopeList({ ordering: "scope_name" })
 | 
			
		||||
            .then((propertyMappings: PaginatedScopeMappingList) => {
 | 
			
		||||
                this.propertyMappings = propertyMappings;
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
        new SourcesApi(DEFAULT_CONFIG)
 | 
			
		||||
            .sourcesOauthList({
 | 
			
		||||
                ordering: "name",
 | 
			
		||||
                hasJwks: true,
 | 
			
		||||
            })
 | 
			
		||||
            .then((oauthSources: PaginatedOAuthSourceList) => {
 | 
			
		||||
                this.oauthSources = oauthSources;
 | 
			
		||||
            });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    propertyMappings?: PaginatedScopeMappingList;
 | 
			
		||||
    oauthSources?: PaginatedOAuthSourceList;
 | 
			
		||||
 | 
			
		||||
    @state()
 | 
			
		||||
    showHttpBasic = true;
 | 
			
		||||
 | 
			
		||||
    @state()
 | 
			
		||||
    mode: ProxyMode = ProxyMode.Proxy;
 | 
			
		||||
 | 
			
		||||
    get instance(): ProxyProvider | undefined {
 | 
			
		||||
        return this.wizard.provider as ProxyProvider;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    renderModeDescription(): MaybeTemplateResult {
 | 
			
		||||
        return nothing;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    renderProxyMode() {
 | 
			
		||||
        return html`<h2>This space intentionally left blank</h2>`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    renderHttpBasic(): TemplateResult {
 | 
			
		||||
        return html`<ak-text-input
 | 
			
		||||
                name="basicAuthUserAttribute"
 | 
			
		||||
                label=${msg("HTTP-Basic Username Key")}
 | 
			
		||||
                value="${ifDefined(this.instance?.basicAuthUserAttribute)}"
 | 
			
		||||
                help=${msg(
 | 
			
		||||
                    "User/Group Attribute used for the user part of the HTTP-Basic Header. If not set, the user's Email address is used.",
 | 
			
		||||
                )}
 | 
			
		||||
            >
 | 
			
		||||
            </ak-text-input>
 | 
			
		||||
 | 
			
		||||
            <ak-text-input
 | 
			
		||||
                name="basicAuthPasswordAttribute"
 | 
			
		||||
                label=${msg("HTTP-Basic Password Key")}
 | 
			
		||||
                value="${ifDefined(this.instance?.basicAuthPasswordAttribute)}"
 | 
			
		||||
                help=${msg(
 | 
			
		||||
                    "User/Group Attribute used for the password part of the HTTP-Basic Header.",
 | 
			
		||||
                )}
 | 
			
		||||
            >
 | 
			
		||||
            </ak-text-input>`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render() {
 | 
			
		||||
        return html`<form class="pf-c-form pf-m-horizontal" @input=${this.handleChange}>
 | 
			
		||||
            ${this.renderModeDescription()}
 | 
			
		||||
            <ak-text-input
 | 
			
		||||
                name="name"
 | 
			
		||||
                value=${ifDefined(this.instance?.name)}
 | 
			
		||||
                required
 | 
			
		||||
                label=${msg("Name")}
 | 
			
		||||
            ></ak-text-input>
 | 
			
		||||
 | 
			
		||||
            <ak-form-element-horizontal
 | 
			
		||||
                label=${msg("Authentication flow")}
 | 
			
		||||
                ?required=${false}
 | 
			
		||||
                name="authenticationFlow"
 | 
			
		||||
            >
 | 
			
		||||
                <ak-flow-search
 | 
			
		||||
                    flowType=${FlowsInstancesListDesignationEnum.Authentication}
 | 
			
		||||
                    .currentFlow=${this.instance?.authenticationFlow}
 | 
			
		||||
                    required
 | 
			
		||||
                ></ak-flow-search>
 | 
			
		||||
                <p class="pf-c-form__helper-text">
 | 
			
		||||
                    ${msg("Flow used when a user access this provider and is not authenticated.")}
 | 
			
		||||
                </p>
 | 
			
		||||
            </ak-form-element-horizontal>
 | 
			
		||||
 | 
			
		||||
            <ak-form-element-horizontal
 | 
			
		||||
                label=${msg("Authorization flow")}
 | 
			
		||||
                ?required=${true}
 | 
			
		||||
                name="authorizationFlow"
 | 
			
		||||
            >
 | 
			
		||||
                <ak-flow-search
 | 
			
		||||
                    flowType=${FlowsInstancesListDesignationEnum.Authorization}
 | 
			
		||||
                    .currentFlow=${this.instance?.authorizationFlow}
 | 
			
		||||
                    required
 | 
			
		||||
                ></ak-flow-search>
 | 
			
		||||
                <p class="pf-c-form__helper-text">
 | 
			
		||||
                    ${msg("Flow used when authorizing this provider.")}
 | 
			
		||||
                </p>
 | 
			
		||||
            </ak-form-element-horizontal>
 | 
			
		||||
 | 
			
		||||
            ${this.renderProxyMode()}
 | 
			
		||||
 | 
			
		||||
            <ak-text-input
 | 
			
		||||
                name="accessTokenValidity"
 | 
			
		||||
                value=${first(this.instance?.accessTokenValidity, "hours=24")}
 | 
			
		||||
                label=${msg("Token validity")}
 | 
			
		||||
                help=${msg("Configure how long tokens are valid for.")}
 | 
			
		||||
            ></ak-text-input>
 | 
			
		||||
 | 
			
		||||
            <ak-form-group>
 | 
			
		||||
                <span slot="header">${msg("Advanced protocol settings")}</span>
 | 
			
		||||
                <div slot="body" class="pf-c-form">
 | 
			
		||||
                    <ak-form-element-horizontal label=${msg("Certificate")} name="certificate">
 | 
			
		||||
                        <ak-crypto-certificate-search
 | 
			
		||||
                            certificate=${ifDefined(this.instance?.certificate ?? undefined)}
 | 
			
		||||
                        ></ak-crypto-certificate-search>
 | 
			
		||||
                    </ak-form-element-horizontal>
 | 
			
		||||
 | 
			
		||||
                    <ak-form-element-horizontal
 | 
			
		||||
                        label=${msg("Additional scopes")}
 | 
			
		||||
                        name="propertyMappings"
 | 
			
		||||
                    >
 | 
			
		||||
                        <select class="pf-c-form-control" multiple>
 | 
			
		||||
                            ${this.propertyMappings?.results
 | 
			
		||||
                                .filter((scope) => {
 | 
			
		||||
                                    return !scope.managed?.startsWith("goauthentik.io/providers");
 | 
			
		||||
                                })
 | 
			
		||||
                                .map((scope) => {
 | 
			
		||||
                                    const selected = (this.instance?.propertyMappings || []).some(
 | 
			
		||||
                                        (su) => {
 | 
			
		||||
                                            return su == scope.pk;
 | 
			
		||||
                                        },
 | 
			
		||||
                                    );
 | 
			
		||||
                                    return html`<option
 | 
			
		||||
                                        value=${ifDefined(scope.pk)}
 | 
			
		||||
                                        ?selected=${selected}
 | 
			
		||||
                                    >
 | 
			
		||||
                                        ${scope.name}
 | 
			
		||||
                                    </option>`;
 | 
			
		||||
                                })}
 | 
			
		||||
                        </select>
 | 
			
		||||
                        <p class="pf-c-form__helper-text">
 | 
			
		||||
                            ${msg("Additional scope mappings, which are passed to the proxy.")}
 | 
			
		||||
                        </p>
 | 
			
		||||
                        <p class="pf-c-form__helper-text">
 | 
			
		||||
                            ${msg("Hold control/command to select multiple items.")}
 | 
			
		||||
                        </p>
 | 
			
		||||
                    </ak-form-element-horizontal>
 | 
			
		||||
 | 
			
		||||
                    <ak-textarea-input
 | 
			
		||||
                        name="skipPathRegex"
 | 
			
		||||
                        label=${this.mode === ProxyMode.ForwardDomain
 | 
			
		||||
                            ? msg("Unauthenticated URLs")
 | 
			
		||||
                            : msg("Unauthenticated Paths")}
 | 
			
		||||
                        value=${ifDefined(this.instance?.skipPathRegex)}
 | 
			
		||||
                        .bighelp=${html` <p class="pf-c-form__helper-text">
 | 
			
		||||
                                ${msg(
 | 
			
		||||
                                    "Regular expressions for which authentication is not required. Each new line is interpreted as a new expression.",
 | 
			
		||||
                                )}
 | 
			
		||||
                            </p>
 | 
			
		||||
                            <p class="pf-c-form__helper-text">
 | 
			
		||||
                                ${msg(
 | 
			
		||||
                                    "When using proxy or forward auth (single application) mode, the requested URL Path is checked against the regular expressions. When using forward auth (domain mode), the full requested URL including scheme and host is matched against the regular expressions.",
 | 
			
		||||
                                )}
 | 
			
		||||
                            </p>`}
 | 
			
		||||
                    >
 | 
			
		||||
                    </ak-textarea-input>
 | 
			
		||||
                </div>
 | 
			
		||||
            </ak-form-group>
 | 
			
		||||
            <ak-form-group>
 | 
			
		||||
                <span slot="header">${msg("Authentication settings")}</span>
 | 
			
		||||
                <div slot="body" class="pf-c-form">
 | 
			
		||||
                    <ak-switch-input
 | 
			
		||||
                        name="interceptHeaderAuth"
 | 
			
		||||
                        ?checked=${first(this.instance?.interceptHeaderAuth, true)}
 | 
			
		||||
                        label=${msg("Intercept header authentication")}
 | 
			
		||||
                        help=${msg(
 | 
			
		||||
                            "When enabled, authentik will intercept the Authorization header to authenticate the request.",
 | 
			
		||||
                        )}
 | 
			
		||||
                    ></ak-switch-input>
 | 
			
		||||
 | 
			
		||||
                    <ak-switch-input
 | 
			
		||||
                        name="basicAuthEnabled"
 | 
			
		||||
                        ?checked=${first(this.instance?.basicAuthEnabled, false)}
 | 
			
		||||
                        @change=${(ev: Event) => {
 | 
			
		||||
                            const el = ev.target as HTMLInputElement;
 | 
			
		||||
                            this.showHttpBasic = el.checked;
 | 
			
		||||
                        }}
 | 
			
		||||
                        label=${msg("Send HTTP-Basic Authentication")}
 | 
			
		||||
                        help=${msg(
 | 
			
		||||
                            "Send a custom HTTP-Basic Authentication header based on values from authentik.",
 | 
			
		||||
                        )}
 | 
			
		||||
                    ></ak-switch-input>
 | 
			
		||||
 | 
			
		||||
                    ${this.showHttpBasic ? this.renderHttpBasic() : html``}
 | 
			
		||||
 | 
			
		||||
                    <ak-form-element-horizontal
 | 
			
		||||
                        label=${msg("Trusted OIDC Sources")}
 | 
			
		||||
                        name="jwksSources"
 | 
			
		||||
                    >
 | 
			
		||||
                        <select class="pf-c-form-control" multiple>
 | 
			
		||||
                            ${this.oauthSources?.results.map((source) => {
 | 
			
		||||
                                const selected = (this.instance?.jwksSources || []).some((su) => {
 | 
			
		||||
                                    return su == source.pk;
 | 
			
		||||
                                });
 | 
			
		||||
                                return html`<option value=${source.pk} ?selected=${selected}>
 | 
			
		||||
                                    ${source.name} (${source.slug})
 | 
			
		||||
                                </option>`;
 | 
			
		||||
                            })}
 | 
			
		||||
                        </select>
 | 
			
		||||
                        <p class="pf-c-form__helper-text">
 | 
			
		||||
                            ${msg(
 | 
			
		||||
                                "JWTs signed by certificates configured in the selected sources can be used to authenticate to this provider.",
 | 
			
		||||
                            )}
 | 
			
		||||
                        </p>
 | 
			
		||||
                        <p class="pf-c-form__helper-text">
 | 
			
		||||
                            ${msg("Hold control/command to select multiple items.")}
 | 
			
		||||
                        </p>
 | 
			
		||||
                    </ak-form-element-horizontal>
 | 
			
		||||
                </div>
 | 
			
		||||
            </ak-form-group>
 | 
			
		||||
        </form>`;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default AkTypeProxyApplicationWizardPage;
 | 
			
		||||
@ -0,0 +1,55 @@
 | 
			
		||||
import "@goauthentik/components/ak-text-input";
 | 
			
		||||
 | 
			
		||||
import { msg } from "@lit/localize";
 | 
			
		||||
import { customElement } from "@lit/reactive-element/decorators.js";
 | 
			
		||||
import { html } from "lit";
 | 
			
		||||
import { ifDefined } from "lit/directives/if-defined.js";
 | 
			
		||||
 | 
			
		||||
import AkTypeProxyApplicationWizardPage from "./AuthenticationByProxyPage";
 | 
			
		||||
 | 
			
		||||
@customElement("ak-application-wizard-authentication-for-forward-proxy-domain")
 | 
			
		||||
export class AkForwardDomainProxyApplicationWizardPage extends AkTypeProxyApplicationWizardPage {
 | 
			
		||||
    renderModeDescription() {
 | 
			
		||||
        return html`<p class="pf-u-mb-xl">
 | 
			
		||||
                ${msg(
 | 
			
		||||
                    "Use this provider with nginx's auth_request or traefik's forwardAuth. Only a single provider is required per root domain. You can't do per-application authorization, but you don't have to create a provider for each application.",
 | 
			
		||||
                )}
 | 
			
		||||
            </p>
 | 
			
		||||
            <div class="pf-u-mb-xl">
 | 
			
		||||
                ${msg("An example setup can look like this:")}
 | 
			
		||||
                <ul class="pf-c-list">
 | 
			
		||||
                    <li>${msg("authentik running on auth.example.com")}</li>
 | 
			
		||||
                    <li>${msg("app1 running on app1.example.com")}</li>
 | 
			
		||||
                </ul>
 | 
			
		||||
                ${msg(
 | 
			
		||||
                    "In this case, you'd set the Authentication URL to auth.example.com and Cookie domain to example.com.",
 | 
			
		||||
                )}
 | 
			
		||||
            </div>`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    renderProxyMode() {
 | 
			
		||||
        return html`
 | 
			
		||||
            <ak-text-input
 | 
			
		||||
                name="externalHost"
 | 
			
		||||
                label=${msg("External host")}
 | 
			
		||||
                value=${ifDefined(this.instance?.externalHost)}
 | 
			
		||||
                required
 | 
			
		||||
                help=${msg(
 | 
			
		||||
                    "The external URL you'll authenticate at. The authentik core server should be reachable under this URL.",
 | 
			
		||||
                )}
 | 
			
		||||
            >
 | 
			
		||||
            </ak-text-input>
 | 
			
		||||
            <ak-text-input
 | 
			
		||||
                name="cookieDomain"
 | 
			
		||||
                label=${msg("Cookie domain")}
 | 
			
		||||
                value="${ifDefined(this.instance?.cookieDomain)}"
 | 
			
		||||
                required
 | 
			
		||||
                help=${msg(
 | 
			
		||||
                    "Set this to the domain you wish the authentication to be valid for. Must be a parent domain of the URL above. If you're running applications as app1.domain.tld, app2.domain.tld, set this to 'domain.tld'.",
 | 
			
		||||
                )}
 | 
			
		||||
            ></ak-text-input>
 | 
			
		||||
        `;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default AkForwardDomainProxyApplicationWizardPage;
 | 
			
		||||
@ -0,0 +1,49 @@
 | 
			
		||||
import { first } from "@goauthentik/common/utils";
 | 
			
		||||
import "@goauthentik/components/ak-switch-input";
 | 
			
		||||
import "@goauthentik/components/ak-text-input";
 | 
			
		||||
 | 
			
		||||
import { msg } from "@lit/localize";
 | 
			
		||||
import { customElement } from "@lit/reactive-element/decorators.js";
 | 
			
		||||
import { html } from "lit";
 | 
			
		||||
import { ifDefined } from "lit/directives/if-defined.js";
 | 
			
		||||
 | 
			
		||||
import AkTypeProxyApplicationWizardPage from "./AuthenticationByProxyPage";
 | 
			
		||||
 | 
			
		||||
@customElement("ak-application-wizard-authentication-for-reverse-proxy")
 | 
			
		||||
export class AkReverseProxyApplicationWizardPage extends AkTypeProxyApplicationWizardPage {
 | 
			
		||||
    renderModeDescription() {
 | 
			
		||||
        return html`<p class="pf-u-mb-xl">
 | 
			
		||||
            ${msg(
 | 
			
		||||
                "This provider will behave like a transparent reverse-proxy, except requests must be authenticated. If your upstream application uses HTTPS, make sure to connect to the outpost using HTTPS as well.",
 | 
			
		||||
            )}
 | 
			
		||||
        </p>`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    renderProxyMode() {
 | 
			
		||||
        return html` <ak-text-input
 | 
			
		||||
                name="externalHost"
 | 
			
		||||
                value=${ifDefined(this.instance?.externalHost)}
 | 
			
		||||
                required
 | 
			
		||||
                label=${msg("External host")}
 | 
			
		||||
                help=${msg(
 | 
			
		||||
                    "The external URL you'll access the application at. Include any non-standard port.",
 | 
			
		||||
                )}
 | 
			
		||||
            ></ak-text-input>
 | 
			
		||||
            <ak-text-input
 | 
			
		||||
                name="internalHost"
 | 
			
		||||
                value=${ifDefined(this.instance?.internalHost)}
 | 
			
		||||
                required
 | 
			
		||||
                label=${msg("Internal host")}
 | 
			
		||||
                help=${msg("Upstream host that the requests are forwarded to.")}
 | 
			
		||||
            ></ak-text-input>
 | 
			
		||||
            <ak-switch-input
 | 
			
		||||
                name="internalHostSslValidation"
 | 
			
		||||
                ?checked=${first(this.instance?.internalHostSslValidation, true)}
 | 
			
		||||
                label=${msg("Internal host SSL Validation")}
 | 
			
		||||
                help=${msg("Validate SSL Certificates of upstream servers.")}
 | 
			
		||||
            >
 | 
			
		||||
            </ak-switch-input>`;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default AkReverseProxyApplicationWizardPage;
 | 
			
		||||
@ -0,0 +1,36 @@
 | 
			
		||||
import "@goauthentik/components/ak-text-input";
 | 
			
		||||
 | 
			
		||||
import { msg } from "@lit/localize";
 | 
			
		||||
import { customElement } from "@lit/reactive-element/decorators.js";
 | 
			
		||||
import { html } from "lit";
 | 
			
		||||
import { ifDefined } from "lit/directives/if-defined.js";
 | 
			
		||||
 | 
			
		||||
import AkTypeProxyApplicationWizardPage from "./AuthenticationByProxyPage";
 | 
			
		||||
 | 
			
		||||
@customElement("ak-application-wizard-authentication-for-single-forward-proxy")
 | 
			
		||||
export class AkForwardSingleProxyApplicationWizardPage extends AkTypeProxyApplicationWizardPage {
 | 
			
		||||
    renderModeDescription() {
 | 
			
		||||
        return html`<p class="pf-u-mb-xl">
 | 
			
		||||
            ${msg(
 | 
			
		||||
                html`Use this provider with nginx's <code>auth_request</code> or traefik's
 | 
			
		||||
                    <code>forwardAuth</code>. Each application/domain needs its own provider.
 | 
			
		||||
                    Additionally, on each domain, <code>/outpost.goauthentik.io</code> must be
 | 
			
		||||
                    routed to the outpost (when using a managed outpost, this is done for you).`,
 | 
			
		||||
            )}
 | 
			
		||||
        </p>`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    renderProxyMode() {
 | 
			
		||||
        return html`<ak-text-input
 | 
			
		||||
            name="externalHost"
 | 
			
		||||
            value=${ifDefined(this.instance?.externalHost)}
 | 
			
		||||
            required
 | 
			
		||||
            label=${msg("External host")}
 | 
			
		||||
            help=${msg(
 | 
			
		||||
                "The external URL you'll access the application at. Include any non-standard port.",
 | 
			
		||||
            )}
 | 
			
		||||
        ></ak-text-input>`;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default AkForwardSingleProxyApplicationWizardPage;
 | 
			
		||||
@ -0,0 +1,73 @@
 | 
			
		||||
import "@goauthentik/admin/common/ak-crypto-certificate-search";
 | 
			
		||||
import "@goauthentik/admin/common/ak-flow-search/ak-tenanted-flow-search";
 | 
			
		||||
import { ascii_letters, digits, first, randomString } from "@goauthentik/common/utils";
 | 
			
		||||
import "@goauthentik/components/ak-text-input";
 | 
			
		||||
import { rootInterface } from "@goauthentik/elements/Base";
 | 
			
		||||
import "@goauthentik/elements/forms/FormGroup";
 | 
			
		||||
import "@goauthentik/elements/forms/HorizontalFormElement";
 | 
			
		||||
 | 
			
		||||
import { msg } from "@lit/localize";
 | 
			
		||||
import { customElement } from "@lit/reactive-element/decorators.js";
 | 
			
		||||
import { html } from "lit";
 | 
			
		||||
import { ifDefined } from "lit/directives/if-defined.js";
 | 
			
		||||
 | 
			
		||||
import { FlowsInstancesListDesignationEnum, RadiusProvider } from "@goauthentik/api";
 | 
			
		||||
 | 
			
		||||
import BaseProviderPanel from "../BaseProviderPanel";
 | 
			
		||||
 | 
			
		||||
@customElement("ak-application-wizard-authentication-by-radius")
 | 
			
		||||
export class ApplicationWizardAuthenticationByRadius extends BaseProviderPanel {
 | 
			
		||||
    render() {
 | 
			
		||||
        const provider = this.wizard.provider as RadiusProvider | undefined;
 | 
			
		||||
 | 
			
		||||
        return html`<form class="pf-c-form pf-m-horizontal" @input=${this.handleChange}>
 | 
			
		||||
            <ak-text-input
 | 
			
		||||
                name="name"
 | 
			
		||||
                label=${msg("Name")}
 | 
			
		||||
                value=${ifDefined(provider?.name)}
 | 
			
		||||
                required
 | 
			
		||||
            >
 | 
			
		||||
            </ak-text-input>
 | 
			
		||||
 | 
			
		||||
            <ak-form-element-horizontal
 | 
			
		||||
                label=${msg("Authentication flow")}
 | 
			
		||||
                ?required=${true}
 | 
			
		||||
                name="authorizationFlow"
 | 
			
		||||
            >
 | 
			
		||||
                <ak-tenanted-flow-search
 | 
			
		||||
                    flowType=${FlowsInstancesListDesignationEnum.Authentication}
 | 
			
		||||
                    .currentFlow=${provider?.authorizationFlow}
 | 
			
		||||
                    .tenantFlow=${rootInterface()?.tenant?.flowAuthentication}
 | 
			
		||||
                    required
 | 
			
		||||
                ></ak-tenanted-flow-search>
 | 
			
		||||
                <p class="pf-c-form__helper-text">${msg("Flow used for users to authenticate.")}</p>
 | 
			
		||||
            </ak-form-element-horizontal>
 | 
			
		||||
 | 
			
		||||
            <ak-form-group expanded>
 | 
			
		||||
                <span slot="header"> ${msg("Protocol settings")} </span>
 | 
			
		||||
                <div slot="body" class="pf-c-form">
 | 
			
		||||
                    <ak-text-input
 | 
			
		||||
                        name="sharedSecret"
 | 
			
		||||
                        label=${msg("Shared secret")}
 | 
			
		||||
                        value=${first(
 | 
			
		||||
                            provider?.sharedSecret,
 | 
			
		||||
                            randomString(128, ascii_letters + digits),
 | 
			
		||||
                        )}
 | 
			
		||||
                        required
 | 
			
		||||
                    ></ak-text-input>
 | 
			
		||||
                    <ak-text-input
 | 
			
		||||
                        name="clientNetworks"
 | 
			
		||||
                        label=${msg("Client Networks")}
 | 
			
		||||
                        value=${first(provider?.clientNetworks, "0.0.0.0/0, ::/0")}
 | 
			
		||||
                        required
 | 
			
		||||
                        help=${msg(`List of CIDRs (comma-seperated) that clients can connect from. A more specific
 | 
			
		||||
                            CIDR will match before a looser one. Clients connecting from a non-specified CIDR
 | 
			
		||||
                            will be dropped.`)}
 | 
			
		||||
                    ></ak-text-input>
 | 
			
		||||
                </div>
 | 
			
		||||
            </ak-form-group>
 | 
			
		||||
        </form>`;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default ApplicationWizardAuthenticationByRadius;
 | 
			
		||||
@ -0,0 +1,33 @@
 | 
			
		||||
import { msg } from "@lit/localize";
 | 
			
		||||
 | 
			
		||||
import { DigestAlgorithmEnum, SignatureAlgorithmEnum, SpBindingEnum } from "@goauthentik/api";
 | 
			
		||||
 | 
			
		||||
type Option<T> = [string, T, boolean?];
 | 
			
		||||
 | 
			
		||||
function toOptions<T>(options: Option<T>[]) {
 | 
			
		||||
    return options.map(([label, value, isDefault]: Option<T>) => ({
 | 
			
		||||
        label,
 | 
			
		||||
        value,
 | 
			
		||||
        default: isDefault ?? false,
 | 
			
		||||
    }));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const spBindingOptions = toOptions([
 | 
			
		||||
    [msg("Redirect"), SpBindingEnum.Redirect, true],
 | 
			
		||||
    [msg("Post"), SpBindingEnum.Post],
 | 
			
		||||
]);
 | 
			
		||||
 | 
			
		||||
export const digestAlgorithmOptions = toOptions([
 | 
			
		||||
    ["SHA1", DigestAlgorithmEnum._200009Xmldsigsha1],
 | 
			
		||||
    ["SHA256", DigestAlgorithmEnum._200104Xmlencsha256, true],
 | 
			
		||||
    ["SHA384", DigestAlgorithmEnum._200104XmldsigMoresha384],
 | 
			
		||||
    ["SHA512", DigestAlgorithmEnum._200104Xmlencsha512],
 | 
			
		||||
]);
 | 
			
		||||
 | 
			
		||||
export const signatureAlgorithmOptions = toOptions([
 | 
			
		||||
    ["RSA-SHA1", SignatureAlgorithmEnum._200009XmldsigrsaSha1],
 | 
			
		||||
    ["RSA-SHA256", SignatureAlgorithmEnum._200104XmldsigMorersaSha256, true],
 | 
			
		||||
    ["RSA-SHA384", SignatureAlgorithmEnum._200104XmldsigMorersaSha384],
 | 
			
		||||
    ["RSA-SHA512", SignatureAlgorithmEnum._200104XmldsigMorersaSha512],
 | 
			
		||||
    ["DSA-SHA1", SignatureAlgorithmEnum._200009XmldsigdsaSha1],
 | 
			
		||||
]);
 | 
			
		||||
@ -0,0 +1,250 @@
 | 
			
		||||
import "@goauthentik/admin/common/ak-core-group-search";
 | 
			
		||||
import "@goauthentik/admin/common/ak-crypto-certificate-search";
 | 
			
		||||
import "@goauthentik/admin/common/ak-flow-search/ak-tenanted-flow-search";
 | 
			
		||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
 | 
			
		||||
import "@goauthentik/components/ak-number-input";
 | 
			
		||||
import "@goauthentik/components/ak-radio-input";
 | 
			
		||||
import "@goauthentik/components/ak-switch-input";
 | 
			
		||||
import "@goauthentik/components/ak-text-input";
 | 
			
		||||
import "@goauthentik/elements/forms/FormGroup";
 | 
			
		||||
import "@goauthentik/elements/forms/HorizontalFormElement";
 | 
			
		||||
 | 
			
		||||
import { msg } from "@lit/localize";
 | 
			
		||||
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
 | 
			
		||||
import { html } from "lit";
 | 
			
		||||
import { ifDefined } from "lit/directives/if-defined.js";
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
    FlowsInstancesListDesignationEnum,
 | 
			
		||||
    PaginatedSAMLPropertyMappingList,
 | 
			
		||||
    PropertymappingsApi,
 | 
			
		||||
    SAMLProvider,
 | 
			
		||||
} from "@goauthentik/api";
 | 
			
		||||
 | 
			
		||||
import BaseProviderPanel from "../BaseProviderPanel";
 | 
			
		||||
import {
 | 
			
		||||
    digestAlgorithmOptions,
 | 
			
		||||
    signatureAlgorithmOptions,
 | 
			
		||||
    spBindingOptions,
 | 
			
		||||
} from "./SamlProviderOptions";
 | 
			
		||||
 | 
			
		||||
@customElement("ak-application-wizard-authentication-by-saml-configuration")
 | 
			
		||||
export class ApplicationWizardProviderSamlConfiguration extends BaseProviderPanel {
 | 
			
		||||
    propertyMappings?: PaginatedSAMLPropertyMappingList;
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        super();
 | 
			
		||||
        new PropertymappingsApi(DEFAULT_CONFIG)
 | 
			
		||||
            .propertymappingsSamlList({
 | 
			
		||||
                ordering: "saml_name",
 | 
			
		||||
            })
 | 
			
		||||
            .then((propertyMappings: PaginatedSAMLPropertyMappingList) => {
 | 
			
		||||
                this.propertyMappings = propertyMappings;
 | 
			
		||||
            });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render() {
 | 
			
		||||
        const provider = this.wizard.provider as SAMLProvider | undefined;
 | 
			
		||||
 | 
			
		||||
        return html` <form class="pf-c-form pf-m-horizontal" @input=${this.handleChange}>
 | 
			
		||||
            <ak-text-input
 | 
			
		||||
                name="name"
 | 
			
		||||
                value=${ifDefined(provider?.name)}
 | 
			
		||||
                required
 | 
			
		||||
                label=${msg("Name")}
 | 
			
		||||
            ></ak-text-input>
 | 
			
		||||
 | 
			
		||||
            <ak-form-element-horizontal
 | 
			
		||||
                label=${msg("Authentication flow")}
 | 
			
		||||
                ?required=${false}
 | 
			
		||||
                name="authenticationFlow"
 | 
			
		||||
            >
 | 
			
		||||
                <ak-flow-search
 | 
			
		||||
                    flowType=${FlowsInstancesListDesignationEnum.Authentication}
 | 
			
		||||
                    .currentFlow=${provider?.authenticationFlow}
 | 
			
		||||
                    required
 | 
			
		||||
                ></ak-flow-search>
 | 
			
		||||
                <p class="pf-c-form__helper-text">
 | 
			
		||||
                    ${msg("Flow used when a user access this provider and is not authenticated.")}
 | 
			
		||||
                </p>
 | 
			
		||||
            </ak-form-element-horizontal>
 | 
			
		||||
 | 
			
		||||
            <ak-form-element-horizontal
 | 
			
		||||
                label=${msg("Authorization flow")}
 | 
			
		||||
                ?required=${true}
 | 
			
		||||
                name="authorizationFlow"
 | 
			
		||||
            >
 | 
			
		||||
                <ak-flow-search
 | 
			
		||||
                    flowType=${FlowsInstancesListDesignationEnum.Authorization}
 | 
			
		||||
                    .currentFlow=${provider?.authorizationFlow}
 | 
			
		||||
                    required
 | 
			
		||||
                ></ak-flow-search>
 | 
			
		||||
                <p class="pf-c-form__helper-text">
 | 
			
		||||
                    ${msg("Flow used when authorizing this provider.")}
 | 
			
		||||
                </p>
 | 
			
		||||
            </ak-form-element-horizontal>
 | 
			
		||||
 | 
			
		||||
            <ak-form-group .expanded=${true}>
 | 
			
		||||
                <span slot="header"> ${msg("Protocol settings")} </span>
 | 
			
		||||
                <div slot="body" class="pf-c-form">
 | 
			
		||||
                    <ak-text-input
 | 
			
		||||
                        name="acsUrl"
 | 
			
		||||
                        value=${ifDefined(provider?.acsUrl)}
 | 
			
		||||
                        required
 | 
			
		||||
                        label=${msg("ACS URL")}
 | 
			
		||||
                    ></ak-text-input>
 | 
			
		||||
 | 
			
		||||
                    <ak-text-input
 | 
			
		||||
                        name="issuer"
 | 
			
		||||
                        value=${provider?.issuer || "authentik"}
 | 
			
		||||
                        required
 | 
			
		||||
                        label=${msg("Issuer")}
 | 
			
		||||
                        help=${msg("Also known as EntityID.")}
 | 
			
		||||
                    ></ak-text-input>
 | 
			
		||||
 | 
			
		||||
                    <ak-radio-input
 | 
			
		||||
                        name="spBinding"
 | 
			
		||||
                        label=${msg("Service Provider Binding")}
 | 
			
		||||
                        required
 | 
			
		||||
                        .options=${spBindingOptions}
 | 
			
		||||
                        .value=${provider?.spBinding}
 | 
			
		||||
                        help=${msg(
 | 
			
		||||
                            "Determines how authentik sends the response back to the Service Provider.",
 | 
			
		||||
                        )}
 | 
			
		||||
                    >
 | 
			
		||||
                    </ak-radio-input>
 | 
			
		||||
 | 
			
		||||
                    <ak-text-input
 | 
			
		||||
                        name="audience"
 | 
			
		||||
                        value=${ifDefined(provider?.audience)}
 | 
			
		||||
                        label=${msg("Audience")}
 | 
			
		||||
                    ></ak-text-input>
 | 
			
		||||
                </div>
 | 
			
		||||
            </ak-form-group>
 | 
			
		||||
 | 
			
		||||
            <ak-form-group>
 | 
			
		||||
                <span slot="header"> ${msg("Advanced protocol settings")} </span>
 | 
			
		||||
                <div slot="body" class="pf-c-form">
 | 
			
		||||
                    <ak-form-element-horizontal
 | 
			
		||||
                        label=${msg("Signing Certificate")}
 | 
			
		||||
                        name="signingKp"
 | 
			
		||||
                    >
 | 
			
		||||
                        <ak-crypto-certificate-search
 | 
			
		||||
                            certificate=${ifDefined(provider?.signingKp ?? undefined)}
 | 
			
		||||
                        ></ak-crypto-certificate-search>
 | 
			
		||||
                        <p class="pf-c-form__helper-text">
 | 
			
		||||
                            ${msg(
 | 
			
		||||
                                "Certificate used to sign outgoing Responses going to the Service Provider.",
 | 
			
		||||
                            )}
 | 
			
		||||
                        </p>
 | 
			
		||||
                    </ak-form-element-horizontal>
 | 
			
		||||
 | 
			
		||||
                    <ak-form-element-horizontal
 | 
			
		||||
                        label=${msg("Verification Certificate")}
 | 
			
		||||
                        name="verificationKp"
 | 
			
		||||
                    >
 | 
			
		||||
                        <ak-crypto-certificate-search
 | 
			
		||||
                            certificate=${ifDefined(provider?.verificationKp ?? undefined)}
 | 
			
		||||
                            nokey
 | 
			
		||||
                        ></ak-crypto-certificate-search>
 | 
			
		||||
                        <p class="pf-c-form__helper-text">
 | 
			
		||||
                            ${msg(
 | 
			
		||||
                                "When selected, incoming assertion's Signatures will be validated against this certificate. To allow unsigned Requests, leave on default.",
 | 
			
		||||
                            )}
 | 
			
		||||
                        </p>
 | 
			
		||||
                    </ak-form-element-horizontal>
 | 
			
		||||
 | 
			
		||||
                    <ak-form-element-horizontal
 | 
			
		||||
                        label=${msg("Property mappings")}
 | 
			
		||||
                        ?required=${true}
 | 
			
		||||
                        name="propertyMappings"
 | 
			
		||||
                    >
 | 
			
		||||
                        <select class="pf-c-form-control" multiple>
 | 
			
		||||
                            ${this.propertyMappings?.results.map((mapping) => {
 | 
			
		||||
                                let selected = false;
 | 
			
		||||
                                if (!provider?.propertyMappings) {
 | 
			
		||||
                                    selected =
 | 
			
		||||
                                        mapping.managed?.startsWith(
 | 
			
		||||
                                            "goauthentik.io/providers/saml",
 | 
			
		||||
                                        ) || false;
 | 
			
		||||
                                } else {
 | 
			
		||||
                                    selected = Array.from(provider?.propertyMappings).some((su) => {
 | 
			
		||||
                                        return su == mapping.pk;
 | 
			
		||||
                                    });
 | 
			
		||||
                                }
 | 
			
		||||
                                return html`<option
 | 
			
		||||
                                    value=${ifDefined(mapping.pk)}
 | 
			
		||||
                                    ?selected=${selected}
 | 
			
		||||
                                >
 | 
			
		||||
                                    ${mapping.name}
 | 
			
		||||
                                </option>`;
 | 
			
		||||
                            })}
 | 
			
		||||
                        </select>
 | 
			
		||||
                        <p class="pf-c-form__helper-text">
 | 
			
		||||
                            ${msg("Hold control/command to select multiple items.")}
 | 
			
		||||
                        </p>
 | 
			
		||||
                    </ak-form-element-horizontal>
 | 
			
		||||
 | 
			
		||||
                    <ak-form-element-horizontal
 | 
			
		||||
                        label=${msg("NameID Property Mapping")}
 | 
			
		||||
                        name="nameIdMapping"
 | 
			
		||||
                    >
 | 
			
		||||
                        <ak-saml-property-mapping-search
 | 
			
		||||
                            name="nameIdMapping"
 | 
			
		||||
                            propertymapping=${ifDefined(provider?.nameIdMapping ?? undefined)}
 | 
			
		||||
                        ></ak-saml-property-mapping-search>
 | 
			
		||||
                        <p class="pf-c-form__helper-text">
 | 
			
		||||
                            ${msg(
 | 
			
		||||
                                "Configure how the NameID value will be created. When left empty, the NameIDPolicy of the incoming request will be respected.",
 | 
			
		||||
                            )}
 | 
			
		||||
                        </p>
 | 
			
		||||
                    </ak-form-element-horizontal>
 | 
			
		||||
 | 
			
		||||
                    <ak-text-input
 | 
			
		||||
                        name="assertionValidNotBefore"
 | 
			
		||||
                        value=${provider?.assertionValidNotBefore || "minutes=-5"}
 | 
			
		||||
                        required
 | 
			
		||||
                        label=${msg("Assertion valid not before")}
 | 
			
		||||
                        help=${msg("Configure the maximum allowed time drift for an assertion.")}
 | 
			
		||||
                    ></ak-text-input>
 | 
			
		||||
 | 
			
		||||
                    <ak-text-input
 | 
			
		||||
                        name="assertionValidNotOnOrAfter"
 | 
			
		||||
                        value=${provider?.assertionValidNotOnOrAfter || "minutes=5"}
 | 
			
		||||
                        required
 | 
			
		||||
                        label=${msg("Assertion valid not on or after")}
 | 
			
		||||
                        help=${msg("Assertion not valid on or after current time + this value.")}
 | 
			
		||||
                    ></ak-text-input>
 | 
			
		||||
 | 
			
		||||
                    <ak-text-input
 | 
			
		||||
                        name="sessionValidNotOnOrAfter"
 | 
			
		||||
                        value=${provider?.sessionValidNotOnOrAfter || "minutes=86400"}
 | 
			
		||||
                        required
 | 
			
		||||
                        label=${msg("Session valid not on or after")}
 | 
			
		||||
                        help=${msg("Session not valid on or after current time + this value.")}
 | 
			
		||||
                    ></ak-text-input>
 | 
			
		||||
 | 
			
		||||
                    <ak-radio-input
 | 
			
		||||
                        name="digestAlgorithm"
 | 
			
		||||
                        label=${msg("Digest algorithm")}
 | 
			
		||||
                        required
 | 
			
		||||
                        .options=${digestAlgorithmOptions}
 | 
			
		||||
                        .value=${provider?.digestAlgorithm}
 | 
			
		||||
                    >
 | 
			
		||||
                    </ak-radio-input>
 | 
			
		||||
 | 
			
		||||
                    <ak-radio-input
 | 
			
		||||
                        name="signatureAlgorithm"
 | 
			
		||||
                        label=${msg("Signature algorithm")}
 | 
			
		||||
                        required
 | 
			
		||||
                        .options=${signatureAlgorithmOptions}
 | 
			
		||||
                        .value=${provider?.signatureAlgorithm}
 | 
			
		||||
                    >
 | 
			
		||||
                    </ak-radio-input>
 | 
			
		||||
                </div>
 | 
			
		||||
            </ak-form-group>
 | 
			
		||||
        </form>`;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default ApplicationWizardProviderSamlConfiguration;
 | 
			
		||||
@ -0,0 +1,81 @@
 | 
			
		||||
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search-no-default";
 | 
			
		||||
import "@goauthentik/components/ak-file-input";
 | 
			
		||||
import { AkFileInput } from "@goauthentik/components/ak-file-input";
 | 
			
		||||
import "@goauthentik/components/ak-text-input";
 | 
			
		||||
import "@goauthentik/elements/forms/HorizontalFormElement";
 | 
			
		||||
 | 
			
		||||
import { msg } from "@lit/localize";
 | 
			
		||||
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
 | 
			
		||||
import { html } from "lit";
 | 
			
		||||
import { query } from "lit/decorators.js";
 | 
			
		||||
import { ifDefined } from "lit/directives/if-defined.js";
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
    FlowsInstancesListDesignationEnum,
 | 
			
		||||
    ProvidersSamlImportMetadataCreateRequest,
 | 
			
		||||
} from "@goauthentik/api";
 | 
			
		||||
 | 
			
		||||
import BaseProviderPanel from "../BaseProviderPanel";
 | 
			
		||||
 | 
			
		||||
@customElement("ak-application-wizard-authentication-by-saml-import")
 | 
			
		||||
export class ApplicationWizardProviderSamlImport extends BaseProviderPanel {
 | 
			
		||||
    @query('ak-file-input[name="metadata"]')
 | 
			
		||||
    fileInput!: AkFileInput;
 | 
			
		||||
 | 
			
		||||
    handleChange(ev: InputEvent) {
 | 
			
		||||
        if (!ev.target) {
 | 
			
		||||
            console.warn(`Received event with no target: ${ev}`);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        const target = ev.target as HTMLInputElement;
 | 
			
		||||
        if (target.type === "file") {
 | 
			
		||||
            const file = this.fileInput.files?.[0] ?? null;
 | 
			
		||||
            if (file) {
 | 
			
		||||
                this.dispatchWizardUpdate({
 | 
			
		||||
                    update: {
 | 
			
		||||
                        provider: {
 | 
			
		||||
                            file,
 | 
			
		||||
                        },
 | 
			
		||||
                    },
 | 
			
		||||
                    status: this.form.checkValidity() ? "valid" : "invalid",
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        super.handleChange(ev);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render() {
 | 
			
		||||
        const provider = this.wizard.provider as
 | 
			
		||||
            | ProvidersSamlImportMetadataCreateRequest
 | 
			
		||||
            | undefined;
 | 
			
		||||
 | 
			
		||||
        return html` <form class="pf-c-form pf-m-horizontal" @input=${this.handleChange}>
 | 
			
		||||
            <ak-text-input
 | 
			
		||||
                name="name"
 | 
			
		||||
                value=${ifDefined(provider?.name)}
 | 
			
		||||
                label=${msg("Name")}
 | 
			
		||||
                required
 | 
			
		||||
                help=${msg("Method's display Name.")}
 | 
			
		||||
            ></ak-text-input>
 | 
			
		||||
 | 
			
		||||
            <ak-form-element-horizontal
 | 
			
		||||
                label=${msg("Authorization flow")}
 | 
			
		||||
                ?required=${true}
 | 
			
		||||
                name="authorizationFlow"
 | 
			
		||||
            >
 | 
			
		||||
                <ak-flow-search-no-default
 | 
			
		||||
                    flowType=${FlowsInstancesListDesignationEnum.Authorization}
 | 
			
		||||
                    required
 | 
			
		||||
                ></ak-flow-search-no-default>
 | 
			
		||||
                <p class="pf-c-form__helper-text">
 | 
			
		||||
                    ${msg("Flow used when authorizing this provider.")}
 | 
			
		||||
                </p>
 | 
			
		||||
            </ak-form-element-horizontal>
 | 
			
		||||
 | 
			
		||||
            <ak-file-input name="metadata" label=${msg("Metadata")} required></ak-file-input>
 | 
			
		||||
        </form>`;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default ApplicationWizardProviderSamlImport;
 | 
			
		||||
@ -0,0 +1,112 @@
 | 
			
		||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
 | 
			
		||||
import { AKElement } from "@goauthentik/elements/Base";
 | 
			
		||||
import { SearchSelect } from "@goauthentik/elements/forms/SearchSelect";
 | 
			
		||||
import { CustomListenerElement } from "@goauthentik/elements/utils/eventEmitter";
 | 
			
		||||
 | 
			
		||||
import { html } from "lit";
 | 
			
		||||
import { customElement } from "lit/decorators.js";
 | 
			
		||||
import { property, query } from "lit/decorators.js";
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
    PropertymappingsApi,
 | 
			
		||||
    PropertymappingsSamlListRequest,
 | 
			
		||||
    SAMLPropertyMapping,
 | 
			
		||||
} from "@goauthentik/api";
 | 
			
		||||
 | 
			
		||||
async function fetchObjects(query?: string): Promise<SAMLPropertyMapping[]> {
 | 
			
		||||
    const args: PropertymappingsSamlListRequest = {
 | 
			
		||||
        ordering: "saml_name",
 | 
			
		||||
    };
 | 
			
		||||
    if (query !== undefined) {
 | 
			
		||||
        args.search = query;
 | 
			
		||||
    }
 | 
			
		||||
    const items = await new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsSamlList(args);
 | 
			
		||||
    return items.results;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function renderElement(item: SAMLPropertyMapping): string {
 | 
			
		||||
    return item.name;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function renderValue(item: SAMLPropertyMapping | undefined): string | undefined {
 | 
			
		||||
    return item?.pk;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * SAML Property Mapping Search
 | 
			
		||||
 *
 | 
			
		||||
 * @element ak-saml-property-mapping-search
 | 
			
		||||
 *
 | 
			
		||||
 * A wrapper around SearchSelect for the SAML Property Search. It's a unique search, but for the
 | 
			
		||||
 * purpose of the form all you need to know is that it is being searched and selected. Let's put the
 | 
			
		||||
 * how somewhere else.
 | 
			
		||||
 *
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
@customElement("ak-saml-property-mapping-search")
 | 
			
		||||
export class SAMLPropertyMappingSearch extends CustomListenerElement(AKElement) {
 | 
			
		||||
    /**
 | 
			
		||||
     * The current property mapping known to the caller.
 | 
			
		||||
     *
 | 
			
		||||
     * @attr
 | 
			
		||||
     */
 | 
			
		||||
    @property({ type: String, reflect: true, attribute: "propertymapping" })
 | 
			
		||||
    propertyMapping?: string;
 | 
			
		||||
 | 
			
		||||
    @query("ak-search-select")
 | 
			
		||||
    search!: SearchSelect<SAMLPropertyMapping>;
 | 
			
		||||
 | 
			
		||||
    @property({ type: String })
 | 
			
		||||
    name: string | null | undefined;
 | 
			
		||||
 | 
			
		||||
    selectedPropertyMapping?: SAMLPropertyMapping;
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        super();
 | 
			
		||||
        this.selected = this.selected.bind(this);
 | 
			
		||||
        this.handleSearchUpdate = this.handleSearchUpdate.bind(this);
 | 
			
		||||
        this.addCustomListener("ak-change", this.handleSearchUpdate);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    get value() {
 | 
			
		||||
        return this.selectedPropertyMapping ? renderValue(this.selectedPropertyMapping) : undefined;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    connectedCallback() {
 | 
			
		||||
        super.connectedCallback();
 | 
			
		||||
        const horizontalContainer = this.closest("ak-form-element-horizontal[name]");
 | 
			
		||||
        if (!horizontalContainer) {
 | 
			
		||||
            throw new Error("This search can only be used in a named ak-form-element-horizontal");
 | 
			
		||||
        }
 | 
			
		||||
        const name = horizontalContainer.getAttribute("name");
 | 
			
		||||
        const myName = this.getAttribute("name");
 | 
			
		||||
        if (name !== null && name !== myName) {
 | 
			
		||||
            this.setAttribute("name", name);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    handleSearchUpdate(ev: CustomEvent) {
 | 
			
		||||
        ev.stopPropagation();
 | 
			
		||||
        this.selectedPropertyMapping = ev.detail.value;
 | 
			
		||||
        this.dispatchEvent(new InputEvent("input", { bubbles: true, composed: true }));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    selected(item: SAMLPropertyMapping): boolean {
 | 
			
		||||
        return this.propertyMapping === item.pk;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render() {
 | 
			
		||||
        return html`
 | 
			
		||||
            <ak-search-select
 | 
			
		||||
                .fetchObjects=${fetchObjects}
 | 
			
		||||
                .renderElement=${renderElement}
 | 
			
		||||
                .value=${renderValue}
 | 
			
		||||
                .selected=${this.selected}
 | 
			
		||||
                blankable
 | 
			
		||||
            >
 | 
			
		||||
            </ak-search-select>
 | 
			
		||||
        `;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default SAMLPropertyMappingSearch;
 | 
			
		||||
@ -0,0 +1,189 @@
 | 
			
		||||
import "@goauthentik/admin/common/ak-crypto-certificate-search";
 | 
			
		||||
import "@goauthentik/admin/common/ak-flow-search/ak-tenanted-flow-search";
 | 
			
		||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
 | 
			
		||||
import { first } from "@goauthentik/common/utils";
 | 
			
		||||
import "@goauthentik/components/ak-switch-input";
 | 
			
		||||
import "@goauthentik/components/ak-text-input";
 | 
			
		||||
import "@goauthentik/elements/forms/FormGroup";
 | 
			
		||||
import "@goauthentik/elements/forms/HorizontalFormElement";
 | 
			
		||||
 | 
			
		||||
import { msg } from "@lit/localize";
 | 
			
		||||
import { customElement, state } from "@lit/reactive-element/decorators.js";
 | 
			
		||||
import { html } from "lit";
 | 
			
		||||
import { ifDefined } from "lit/directives/if-defined.js";
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
    CoreApi,
 | 
			
		||||
    CoreGroupsListRequest,
 | 
			
		||||
    type Group,
 | 
			
		||||
    PaginatedSCIMMappingList,
 | 
			
		||||
    PropertymappingsApi,
 | 
			
		||||
    type SCIMProvider,
 | 
			
		||||
} from "@goauthentik/api";
 | 
			
		||||
 | 
			
		||||
import BaseProviderPanel from "../BaseProviderPanel";
 | 
			
		||||
 | 
			
		||||
@customElement("ak-application-wizard-authentication-by-scim")
 | 
			
		||||
export class ApplicationWizardAuthenticationBySCIM extends BaseProviderPanel {
 | 
			
		||||
    @state()
 | 
			
		||||
    propertyMappings?: PaginatedSCIMMappingList;
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        super();
 | 
			
		||||
        new PropertymappingsApi(DEFAULT_CONFIG)
 | 
			
		||||
            .propertymappingsScopeList({
 | 
			
		||||
                ordering: "scope_name",
 | 
			
		||||
            })
 | 
			
		||||
            .then((propertyMappings: PaginatedSCIMMappingList) => {
 | 
			
		||||
                this.propertyMappings = propertyMappings;
 | 
			
		||||
            });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render() {
 | 
			
		||||
        const provider = this.wizard.provider as SCIMProvider | undefined;
 | 
			
		||||
 | 
			
		||||
        return html`<form class="pf-c-form pf-m-horizontal" @input=${this.handleChange}>
 | 
			
		||||
            <ak-text-input
 | 
			
		||||
                name="name"
 | 
			
		||||
                label=${msg("Name")}
 | 
			
		||||
                value=${ifDefined(provider?.name)}
 | 
			
		||||
                required
 | 
			
		||||
            ></ak-text-input>
 | 
			
		||||
            <ak-form-group expanded>
 | 
			
		||||
                <span slot="header"> ${msg("Protocol settings")} </span>
 | 
			
		||||
                <div slot="body" class="pf-c-form">
 | 
			
		||||
                    <ak-text-input
 | 
			
		||||
                        name="url"
 | 
			
		||||
                        label=${msg("URL")}
 | 
			
		||||
                        value="${first(provider?.url, "")}"
 | 
			
		||||
                        required
 | 
			
		||||
                        help=${msg("SCIM base url, usually ends in /v2.")}
 | 
			
		||||
                    >
 | 
			
		||||
                    </ak-text-input>
 | 
			
		||||
                    <ak-text-input
 | 
			
		||||
                        name="token"
 | 
			
		||||
                        label=${msg("Token")}
 | 
			
		||||
                        value="${first(provider?.token, "")}"
 | 
			
		||||
                        required
 | 
			
		||||
                        help=${msg(
 | 
			
		||||
                            "Token to authenticate with. Currently only bearer authentication is supported.",
 | 
			
		||||
                        )}
 | 
			
		||||
                    >
 | 
			
		||||
                    </ak-text-input>
 | 
			
		||||
                </div>
 | 
			
		||||
            </ak-form-group>
 | 
			
		||||
            <ak-form-group expanded>
 | 
			
		||||
                <span slot="header">${msg("User filtering")}</span>
 | 
			
		||||
                <div slot="body" class="pf-c-form">
 | 
			
		||||
                    <ak-switch-input
 | 
			
		||||
                        name="excludeUsersServiceAccount"
 | 
			
		||||
                        ?checked=${first(provider?.excludeUsersServiceAccount, true)}
 | 
			
		||||
                        label=${msg("Exclude service accounts")}
 | 
			
		||||
                    ></ak-switch-input>
 | 
			
		||||
                    <ak-form-element-horizontal label=${msg("Group")} name="filterGroup">
 | 
			
		||||
                        <ak-search-select
 | 
			
		||||
                            .fetchObjects=${async (query?: string): Promise<Group[]> => {
 | 
			
		||||
                                const args: CoreGroupsListRequest = {
 | 
			
		||||
                                    ordering: "name",
 | 
			
		||||
                                };
 | 
			
		||||
                                if (query !== undefined) {
 | 
			
		||||
                                    args.search = query;
 | 
			
		||||
                                }
 | 
			
		||||
                                const groups = await new CoreApi(DEFAULT_CONFIG).coreGroupsList(
 | 
			
		||||
                                    args,
 | 
			
		||||
                                );
 | 
			
		||||
                                return groups.results;
 | 
			
		||||
                            }}
 | 
			
		||||
                            .renderElement=${(group: Group): string => {
 | 
			
		||||
                                return group.name;
 | 
			
		||||
                            }}
 | 
			
		||||
                            .value=${(group: Group | undefined): string | undefined => {
 | 
			
		||||
                                return group ? group.pk : undefined;
 | 
			
		||||
                            }}
 | 
			
		||||
                            .selected=${(group: Group): boolean => {
 | 
			
		||||
                                return group.pk === provider?.filterGroup;
 | 
			
		||||
                            }}
 | 
			
		||||
                            ?blankable=${true}
 | 
			
		||||
                        >
 | 
			
		||||
                        </ak-search-select>
 | 
			
		||||
                        <p class="pf-c-form__helper-text">
 | 
			
		||||
                            ${msg("Only sync users within the selected group.")}
 | 
			
		||||
                        </p>
 | 
			
		||||
                    </ak-form-element-horizontal>
 | 
			
		||||
                </div>
 | 
			
		||||
            </ak-form-group>
 | 
			
		||||
            <ak-form-group ?expanded=${true}>
 | 
			
		||||
                <span slot="header"> ${msg("Attribute mapping")} </span>
 | 
			
		||||
                <div slot="body" class="pf-c-form">
 | 
			
		||||
                    <ak-form-element-horizontal
 | 
			
		||||
                        label=${msg("User Property Mappings")}
 | 
			
		||||
                        ?required=${true}
 | 
			
		||||
                        name="propertyMappings"
 | 
			
		||||
                    >
 | 
			
		||||
                        <select class="pf-c-form-control" multiple>
 | 
			
		||||
                            ${this.propertyMappings?.results.map((mapping) => {
 | 
			
		||||
                                let selected = false;
 | 
			
		||||
                                if (!provider?.propertyMappings) {
 | 
			
		||||
                                    selected =
 | 
			
		||||
                                        mapping.managed === "goauthentik.io/providers/scim/user" ||
 | 
			
		||||
                                        false;
 | 
			
		||||
                                } else {
 | 
			
		||||
                                    selected = Array.from(provider?.propertyMappings).some((su) => {
 | 
			
		||||
                                        return su == mapping.pk;
 | 
			
		||||
                                    });
 | 
			
		||||
                                }
 | 
			
		||||
                                return html`<option
 | 
			
		||||
                                    value=${ifDefined(mapping.pk)}
 | 
			
		||||
                                    ?selected=${selected}
 | 
			
		||||
                                >
 | 
			
		||||
                                    ${mapping.name}
 | 
			
		||||
                                </option>`;
 | 
			
		||||
                            })}
 | 
			
		||||
                        </select>
 | 
			
		||||
                        <p class="pf-c-form__helper-text">
 | 
			
		||||
                            ${msg("Property mappings used to user mapping.")}
 | 
			
		||||
                        </p>
 | 
			
		||||
                        <p class="pf-c-form__helper-text">
 | 
			
		||||
                            ${msg("Hold control/command to select multiple items.")}
 | 
			
		||||
                        </p>
 | 
			
		||||
                    </ak-form-element-horizontal>
 | 
			
		||||
                    <ak-form-element-horizontal
 | 
			
		||||
                        label=${msg("Group Property Mappings")}
 | 
			
		||||
                        ?required=${true}
 | 
			
		||||
                        name="propertyMappingsGroup"
 | 
			
		||||
                    >
 | 
			
		||||
                        <select class="pf-c-form-control" multiple>
 | 
			
		||||
                            ${this.propertyMappings?.results.map((mapping) => {
 | 
			
		||||
                                let selected = false;
 | 
			
		||||
                                if (!provider?.propertyMappingsGroup) {
 | 
			
		||||
                                    selected =
 | 
			
		||||
                                        mapping.managed === "goauthentik.io/providers/scim/group";
 | 
			
		||||
                                } else {
 | 
			
		||||
                                    selected = Array.from(provider?.propertyMappingsGroup).some(
 | 
			
		||||
                                        (su) => {
 | 
			
		||||
                                            return su == mapping.pk;
 | 
			
		||||
                                        },
 | 
			
		||||
                                    );
 | 
			
		||||
                                }
 | 
			
		||||
                                return html`<option
 | 
			
		||||
                                    value=${ifDefined(mapping.pk)}
 | 
			
		||||
                                    ?selected=${selected}
 | 
			
		||||
                                >
 | 
			
		||||
                                    ${mapping.name}
 | 
			
		||||
                                </option>`;
 | 
			
		||||
                            })}
 | 
			
		||||
                        </select>
 | 
			
		||||
                        <p class="pf-c-form__helper-text">
 | 
			
		||||
                            ${msg("Property mappings used to group creation.")}
 | 
			
		||||
                        </p>
 | 
			
		||||
                        <p class="pf-c-form__helper-text">
 | 
			
		||||
                            ${msg("Hold control/command to select multiple items.")}
 | 
			
		||||
                        </p>
 | 
			
		||||
                    </ak-form-element-horizontal>
 | 
			
		||||
                </div>
 | 
			
		||||
            </ak-form-group>
 | 
			
		||||
        </form>`;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default ApplicationWizardAuthenticationBySCIM;
 | 
			
		||||
@ -1,35 +0,0 @@
 | 
			
		||||
import "@goauthentik/elements/forms/HorizontalFormElement";
 | 
			
		||||
import { WizardPage } from "@goauthentik/elements/wizard/WizardPage";
 | 
			
		||||
 | 
			
		||||
import { msg } from "@lit/localize";
 | 
			
		||||
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
 | 
			
		||||
import { CSSResult, TemplateResult, html } from "lit";
 | 
			
		||||
 | 
			
		||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
 | 
			
		||||
import PFForm from "@patternfly/patternfly/components/Form/form.css";
 | 
			
		||||
import PFRadio from "@patternfly/patternfly/components/Radio/radio.css";
 | 
			
		||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
 | 
			
		||||
 | 
			
		||||
@customElement("ak-application-wizard-type-oauth-api")
 | 
			
		||||
export class TypeOAuthAPIApplicationWizardPage extends WizardPage {
 | 
			
		||||
    static get styles(): CSSResult[] {
 | 
			
		||||
        return [PFBase, PFButton, PFForm, PFRadio];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    sidebarLabel = () => msg("Method details");
 | 
			
		||||
 | 
			
		||||
    render(): TemplateResult {
 | 
			
		||||
        return html`<form class="pf-c-form pf-m-horizontal">
 | 
			
		||||
            <p>
 | 
			
		||||
                ${msg(
 | 
			
		||||
                    "This configuration can be used to authenticate to authentik with other APIs other otherwise programmatically.",
 | 
			
		||||
                )}
 | 
			
		||||
            </p>
 | 
			
		||||
            <p>
 | 
			
		||||
                ${msg(
 | 
			
		||||
                    "By default, all service accounts can authenticate as this application, as long as they have a valid token of the type app-password.",
 | 
			
		||||
                )}
 | 
			
		||||
            </p>
 | 
			
		||||
        </form> `;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,84 +0,0 @@
 | 
			
		||||
import "@goauthentik/elements/forms/HorizontalFormElement";
 | 
			
		||||
import { WizardPage } from "@goauthentik/elements/wizard/WizardPage";
 | 
			
		||||
 | 
			
		||||
import { msg } from "@lit/localize";
 | 
			
		||||
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
 | 
			
		||||
import { CSSResult, TemplateResult, html } from "lit";
 | 
			
		||||
 | 
			
		||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
 | 
			
		||||
import PFForm from "@patternfly/patternfly/components/Form/form.css";
 | 
			
		||||
import PFRadio from "@patternfly/patternfly/components/Radio/radio.css";
 | 
			
		||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
 | 
			
		||||
 | 
			
		||||
import { TypeCreate } from "@goauthentik/api";
 | 
			
		||||
 | 
			
		||||
@customElement("ak-application-wizard-type-oauth")
 | 
			
		||||
export class TypeOAuthApplicationWizardPage extends WizardPage {
 | 
			
		||||
    applicationTypes: TypeCreate[] = [
 | 
			
		||||
        {
 | 
			
		||||
            component: "ak-application-wizard-type-oauth-code",
 | 
			
		||||
            name: msg("Web application"),
 | 
			
		||||
            description: msg(
 | 
			
		||||
                "Applications which handle the authentication server-side (for example, Python, Go, Rust, Java, PHP)",
 | 
			
		||||
            ),
 | 
			
		||||
            modelName: "",
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            component: "ak-application-wizard-type-oauth-implicit",
 | 
			
		||||
            name: msg("Single-page applications"),
 | 
			
		||||
            description: msg(
 | 
			
		||||
                "Single-page applications which handle authentication in the browser (for example, Javascript, Angular, React, Vue)",
 | 
			
		||||
            ),
 | 
			
		||||
            modelName: "",
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            component: "ak-application-wizard-type-oauth-implicit",
 | 
			
		||||
            name: msg("Native application"),
 | 
			
		||||
            description: msg(
 | 
			
		||||
                "Applications which redirect users to a non-web callback (for example, Android, iOS)",
 | 
			
		||||
            ),
 | 
			
		||||
            modelName: "",
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            component: "ak-application-wizard-type-oauth-api",
 | 
			
		||||
            name: msg("API"),
 | 
			
		||||
            description: msg(
 | 
			
		||||
                "Authentication without user interaction, or machine-to-machine authentication.",
 | 
			
		||||
            ),
 | 
			
		||||
            modelName: "",
 | 
			
		||||
        },
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    static get styles(): CSSResult[] {
 | 
			
		||||
        return [PFBase, PFButton, PFForm, PFRadio];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    sidebarLabel = () => msg("Application type");
 | 
			
		||||
 | 
			
		||||
    render(): TemplateResult {
 | 
			
		||||
        return html`<form class="pf-c-form pf-m-horizontal">
 | 
			
		||||
            ${this.applicationTypes.map((type) => {
 | 
			
		||||
                return html`<div class="pf-c-radio">
 | 
			
		||||
                    <input
 | 
			
		||||
                        class="pf-c-radio__input"
 | 
			
		||||
                        type="radio"
 | 
			
		||||
                        name="type"
 | 
			
		||||
                        id=${type.component}
 | 
			
		||||
                        @change=${() => {
 | 
			
		||||
                            this.host.steps = [
 | 
			
		||||
                                "ak-application-wizard-initial",
 | 
			
		||||
                                "ak-application-wizard-type",
 | 
			
		||||
                                "ak-application-wizard-type-oauth",
 | 
			
		||||
                                type.component,
 | 
			
		||||
                            ];
 | 
			
		||||
                            this.host.state["oauth-type"] = type.component;
 | 
			
		||||
                            this.host.isValid = true;
 | 
			
		||||
                        }}
 | 
			
		||||
                    />
 | 
			
		||||
                    <label class="pf-c-radio__label" for=${type.component}>${type.name}</label>
 | 
			
		||||
                    <span class="pf-c-radio__description">${type.description}</span>
 | 
			
		||||
                </div>`;
 | 
			
		||||
            })}
 | 
			
		||||
        </form> `;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,57 +0,0 @@
 | 
			
		||||
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search-no-default";
 | 
			
		||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
 | 
			
		||||
import { KeyUnknown } from "@goauthentik/elements/forms/Form";
 | 
			
		||||
import "@goauthentik/elements/forms/HorizontalFormElement";
 | 
			
		||||
import "@goauthentik/elements/forms/SearchSelect";
 | 
			
		||||
import { WizardFormPage } from "@goauthentik/elements/wizard/WizardFormPage";
 | 
			
		||||
import "@goauthentik/elements/wizard/WizardFormPage";
 | 
			
		||||
 | 
			
		||||
import { msg } from "@lit/localize";
 | 
			
		||||
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
 | 
			
		||||
import { TemplateResult, html } from "lit";
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
    ClientTypeEnum,
 | 
			
		||||
    FlowsInstancesListDesignationEnum,
 | 
			
		||||
    OAuth2ProviderRequest,
 | 
			
		||||
    ProvidersApi,
 | 
			
		||||
} from "@goauthentik/api";
 | 
			
		||||
 | 
			
		||||
@customElement("ak-application-wizard-type-oauth-code")
 | 
			
		||||
export class TypeOAuthCodeApplicationWizardPage extends WizardFormPage {
 | 
			
		||||
    sidebarLabel = () => msg("Method details");
 | 
			
		||||
 | 
			
		||||
    nextDataCallback = async (data: KeyUnknown): Promise<boolean> => {
 | 
			
		||||
        this.host.addActionBefore(msg("Create provider"), async (): Promise<boolean> => {
 | 
			
		||||
            const req: OAuth2ProviderRequest = {
 | 
			
		||||
                name: this.host.state["name"] as string,
 | 
			
		||||
                clientType: ClientTypeEnum.Confidential,
 | 
			
		||||
                authorizationFlow: data.authorizationFlow as string,
 | 
			
		||||
            };
 | 
			
		||||
            const provider = await new ProvidersApi(DEFAULT_CONFIG).providersOauth2Create({
 | 
			
		||||
                oAuth2ProviderRequest: req,
 | 
			
		||||
            });
 | 
			
		||||
            this.host.state["provider"] = provider;
 | 
			
		||||
            return true;
 | 
			
		||||
        });
 | 
			
		||||
        return true;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    renderForm(): TemplateResult {
 | 
			
		||||
        return html`<form class="pf-c-form pf-m-horizontal">
 | 
			
		||||
            <ak-form-element-horizontal
 | 
			
		||||
                label=${msg("Authorization flow")}
 | 
			
		||||
                ?required=${true}
 | 
			
		||||
                name="authorizationFlow"
 | 
			
		||||
            >
 | 
			
		||||
                <ak-flow-search-no-default
 | 
			
		||||
                    flowType=${FlowsInstancesListDesignationEnum.Authorization}
 | 
			
		||||
                    required
 | 
			
		||||
                ></ak-flow-search-no-default>
 | 
			
		||||
                <p class="pf-c-form__helper-text">
 | 
			
		||||
                    ${msg("Flow used when users access this application.")}
 | 
			
		||||
                </p>
 | 
			
		||||
            </ak-form-element-horizontal>
 | 
			
		||||
        </form>`;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,15 +0,0 @@
 | 
			
		||||
import "@goauthentik/elements/forms/HorizontalFormElement";
 | 
			
		||||
import { WizardFormPage } from "@goauthentik/elements/wizard/WizardFormPage";
 | 
			
		||||
 | 
			
		||||
import { msg } from "@lit/localize";
 | 
			
		||||
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
 | 
			
		||||
import { TemplateResult, html } from "lit";
 | 
			
		||||
 | 
			
		||||
@customElement("ak-application-wizard-type-oauth-implicit")
 | 
			
		||||
export class TypeOAuthImplicitApplicationWizardPage extends WizardFormPage {
 | 
			
		||||
    sidebarLabel = () => msg("Method details");
 | 
			
		||||
 | 
			
		||||
    render(): TemplateResult {
 | 
			
		||||
        return html`<form class="pf-c-form pf-m-horizontal">some stuff idk</form> `;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,64 +0,0 @@
 | 
			
		||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
 | 
			
		||||
import { KeyUnknown } from "@goauthentik/elements/forms/Form";
 | 
			
		||||
import "@goauthentik/elements/forms/HorizontalFormElement";
 | 
			
		||||
import { WizardFormPage } from "@goauthentik/elements/wizard/WizardFormPage";
 | 
			
		||||
 | 
			
		||||
import { msg } from "@lit/localize";
 | 
			
		||||
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
 | 
			
		||||
import { TemplateResult, html } from "lit";
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
    FlowDesignationEnum,
 | 
			
		||||
    FlowsApi,
 | 
			
		||||
    ProvidersApi,
 | 
			
		||||
    ProxyProviderRequest,
 | 
			
		||||
} from "@goauthentik/api";
 | 
			
		||||
 | 
			
		||||
@customElement("ak-application-wizard-type-proxy")
 | 
			
		||||
export class TypeProxyApplicationWizardPage extends WizardFormPage {
 | 
			
		||||
    sidebarLabel = () => msg("Proxy details");
 | 
			
		||||
 | 
			
		||||
    nextDataCallback = async (data: KeyUnknown): Promise<boolean> => {
 | 
			
		||||
        let name = this.host.state["name"] as string;
 | 
			
		||||
        // Check if a provider with the name already exists
 | 
			
		||||
        const providers = await new ProvidersApi(DEFAULT_CONFIG).providersAllList({
 | 
			
		||||
            search: name,
 | 
			
		||||
        });
 | 
			
		||||
        if (providers.results.filter((provider) => provider.name == name)) {
 | 
			
		||||
            name += "-1";
 | 
			
		||||
        }
 | 
			
		||||
        this.host.addActionBefore(msg("Create provider"), async (): Promise<boolean> => {
 | 
			
		||||
            // Get all flows and default to the implicit authorization
 | 
			
		||||
            const flows = await new FlowsApi(DEFAULT_CONFIG).flowsInstancesList({
 | 
			
		||||
                designation: FlowDesignationEnum.Authorization,
 | 
			
		||||
                ordering: "slug",
 | 
			
		||||
            });
 | 
			
		||||
            const req: ProxyProviderRequest = {
 | 
			
		||||
                name: name,
 | 
			
		||||
                authorizationFlow: flows.results[0].pk,
 | 
			
		||||
                externalHost: data.externalHost as string,
 | 
			
		||||
            };
 | 
			
		||||
            const provider = await new ProvidersApi(DEFAULT_CONFIG).providersProxyCreate({
 | 
			
		||||
                proxyProviderRequest: req,
 | 
			
		||||
            });
 | 
			
		||||
            this.host.state["provider"] = provider;
 | 
			
		||||
            return true;
 | 
			
		||||
        });
 | 
			
		||||
        return true;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    renderForm(): TemplateResult {
 | 
			
		||||
        return html`<form class="pf-c-form pf-m-horizontal">
 | 
			
		||||
            <ak-form-element-horizontal
 | 
			
		||||
                label=${msg("External domain")}
 | 
			
		||||
                name="externalHost"
 | 
			
		||||
                ?required=${true}
 | 
			
		||||
            >
 | 
			
		||||
                <input type="text" value="" class="pf-c-form-control" required />
 | 
			
		||||
                <p class="pf-c-form__helper-text">
 | 
			
		||||
                    ${msg("External domain you will be accessing the domain from.")}
 | 
			
		||||
                </p>
 | 
			
		||||
            </ak-form-element-horizontal>
 | 
			
		||||
        </form> `;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,66 +0,0 @@
 | 
			
		||||
import "@goauthentik/elements/forms/HorizontalFormElement";
 | 
			
		||||
import { WizardPage } from "@goauthentik/elements/wizard/WizardPage";
 | 
			
		||||
 | 
			
		||||
import { msg } from "@lit/localize";
 | 
			
		||||
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
 | 
			
		||||
import { CSSResult, TemplateResult, html } from "lit";
 | 
			
		||||
 | 
			
		||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
 | 
			
		||||
import PFForm from "@patternfly/patternfly/components/Form/form.css";
 | 
			
		||||
import PFRadio from "@patternfly/patternfly/components/Radio/radio.css";
 | 
			
		||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
 | 
			
		||||
 | 
			
		||||
import { TypeCreate } from "@goauthentik/api";
 | 
			
		||||
 | 
			
		||||
@customElement("ak-application-wizard-type-saml")
 | 
			
		||||
export class TypeOAuthApplicationWizardPage extends WizardPage {
 | 
			
		||||
    applicationTypes: TypeCreate[] = [
 | 
			
		||||
        {
 | 
			
		||||
            component: "ak-application-wizard-type-saml-import",
 | 
			
		||||
            name: msg("Import SAML Metadata"),
 | 
			
		||||
            description: msg(
 | 
			
		||||
                "Import the metadata document of the applicaation you want to configure.",
 | 
			
		||||
            ),
 | 
			
		||||
            modelName: "",
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            component: "ak-application-wizard-type-saml-config",
 | 
			
		||||
            name: msg("Manual configuration"),
 | 
			
		||||
            description: msg("Manually configure SAML"),
 | 
			
		||||
            modelName: "",
 | 
			
		||||
        },
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    static get styles(): CSSResult[] {
 | 
			
		||||
        return [PFBase, PFButton, PFForm, PFRadio];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    sidebarLabel = () => msg("Application type");
 | 
			
		||||
 | 
			
		||||
    render(): TemplateResult {
 | 
			
		||||
        return html`<form class="pf-c-form pf-m-horizontal">
 | 
			
		||||
            ${this.applicationTypes.map((type) => {
 | 
			
		||||
                return html`<div class="pf-c-radio">
 | 
			
		||||
                    <input
 | 
			
		||||
                        class="pf-c-radio__input"
 | 
			
		||||
                        type="radio"
 | 
			
		||||
                        name="type"
 | 
			
		||||
                        id=${type.component}
 | 
			
		||||
                        @change=${() => {
 | 
			
		||||
                            this.host.steps = [
 | 
			
		||||
                                "ak-application-wizard-initial",
 | 
			
		||||
                                "ak-application-wizard-type",
 | 
			
		||||
                                "ak-application-wizard-type-saml",
 | 
			
		||||
                                type.component,
 | 
			
		||||
                            ];
 | 
			
		||||
                            this.host.state["saml-type"] = type.component;
 | 
			
		||||
                            this.host.isValid = true;
 | 
			
		||||
                        }}
 | 
			
		||||
                    />
 | 
			
		||||
                    <label class="pf-c-radio__label" for=${type.component}>${type.name}</label>
 | 
			
		||||
                    <span class="pf-c-radio__description">${type.description}</span>
 | 
			
		||||
                </div>`;
 | 
			
		||||
            })}
 | 
			
		||||
        </form> `;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,57 +0,0 @@
 | 
			
		||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
 | 
			
		||||
import { KeyUnknown } from "@goauthentik/elements/forms/Form";
 | 
			
		||||
import "@goauthentik/elements/forms/HorizontalFormElement";
 | 
			
		||||
import { WizardFormPage } from "@goauthentik/elements/wizard/WizardFormPage";
 | 
			
		||||
 | 
			
		||||
import { msg } from "@lit/localize";
 | 
			
		||||
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
 | 
			
		||||
import { TemplateResult, html } from "lit";
 | 
			
		||||
 | 
			
		||||
import { FlowDesignationEnum, FlowsApi, ProvidersApi, SAMLProviderRequest } from "@goauthentik/api";
 | 
			
		||||
 | 
			
		||||
@customElement("ak-application-wizard-type-saml-config")
 | 
			
		||||
export class TypeSAMLApplicationWizardPage extends WizardFormPage {
 | 
			
		||||
    sidebarLabel = () => msg("SAML details");
 | 
			
		||||
 | 
			
		||||
    nextDataCallback = async (data: KeyUnknown): Promise<boolean> => {
 | 
			
		||||
        let name = this.host.state["name"] as string;
 | 
			
		||||
        // Check if a provider with the name already exists
 | 
			
		||||
        const providers = await new ProvidersApi(DEFAULT_CONFIG).providersAllList({
 | 
			
		||||
            search: name,
 | 
			
		||||
        });
 | 
			
		||||
        if (providers.results.filter((provider) => provider.name == name)) {
 | 
			
		||||
            name += "-1";
 | 
			
		||||
        }
 | 
			
		||||
        this.host.addActionBefore(msg("Create provider"), async (): Promise<boolean> => {
 | 
			
		||||
            // Get all flows and default to the implicit authorization
 | 
			
		||||
            const flows = await new FlowsApi(DEFAULT_CONFIG).flowsInstancesList({
 | 
			
		||||
                designation: FlowDesignationEnum.Authorization,
 | 
			
		||||
                ordering: "slug",
 | 
			
		||||
            });
 | 
			
		||||
            const req: SAMLProviderRequest = {
 | 
			
		||||
                name: name,
 | 
			
		||||
                authorizationFlow: flows.results[0].pk,
 | 
			
		||||
                acsUrl: data.acsUrl as string,
 | 
			
		||||
            };
 | 
			
		||||
            const provider = await new ProvidersApi(DEFAULT_CONFIG).providersSamlCreate({
 | 
			
		||||
                sAMLProviderRequest: req,
 | 
			
		||||
            });
 | 
			
		||||
            this.host.state["provider"] = provider;
 | 
			
		||||
            return true;
 | 
			
		||||
        });
 | 
			
		||||
        return true;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    renderForm(): TemplateResult {
 | 
			
		||||
        return html`<form class="pf-c-form pf-m-horizontal">
 | 
			
		||||
            <ak-form-element-horizontal label=${msg("ACS URL")} name="acsUrl" ?required=${true}>
 | 
			
		||||
                <input type="text" value="" class="pf-c-form-control" required />
 | 
			
		||||
                <p class="pf-c-form__helper-text">
 | 
			
		||||
                    ${msg(
 | 
			
		||||
                        "URL that authentik will redirect back to after successful authentication.",
 | 
			
		||||
                    )}
 | 
			
		||||
                </p>
 | 
			
		||||
            </ak-form-element-horizontal>
 | 
			
		||||
        </form> `;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,57 +0,0 @@
 | 
			
		||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
 | 
			
		||||
import { KeyUnknown } from "@goauthentik/elements/forms/Form";
 | 
			
		||||
import "@goauthentik/elements/forms/HorizontalFormElement";
 | 
			
		||||
import { WizardFormPage } from "@goauthentik/elements/wizard/WizardFormPage";
 | 
			
		||||
 | 
			
		||||
import { msg } from "@lit/localize";
 | 
			
		||||
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
 | 
			
		||||
import { TemplateResult, html } from "lit";
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
    FlowDesignationEnum,
 | 
			
		||||
    FlowsApi,
 | 
			
		||||
    ProvidersApi,
 | 
			
		||||
    ProvidersSamlImportMetadataCreateRequest,
 | 
			
		||||
} from "@goauthentik/api";
 | 
			
		||||
 | 
			
		||||
@customElement("ak-application-wizard-type-saml-import")
 | 
			
		||||
export class TypeSAMLImportApplicationWizardPage extends WizardFormPage {
 | 
			
		||||
    sidebarLabel = () => msg("Import SAML metadata");
 | 
			
		||||
 | 
			
		||||
    nextDataCallback = async (data: KeyUnknown): Promise<boolean> => {
 | 
			
		||||
        let name = this.host.state["name"] as string;
 | 
			
		||||
        // Check if a provider with the name already exists
 | 
			
		||||
        const providers = await new ProvidersApi(DEFAULT_CONFIG).providersAllList({
 | 
			
		||||
            search: name,
 | 
			
		||||
        });
 | 
			
		||||
        if (providers.results.filter((provider) => provider.name == name)) {
 | 
			
		||||
            name += "-1";
 | 
			
		||||
        }
 | 
			
		||||
        this.host.addActionBefore(msg("Create provider"), async (): Promise<boolean> => {
 | 
			
		||||
            // Get all flows and default to the implicit authorization
 | 
			
		||||
            const flows = await new FlowsApi(DEFAULT_CONFIG).flowsInstancesList({
 | 
			
		||||
                designation: FlowDesignationEnum.Authorization,
 | 
			
		||||
                ordering: "slug",
 | 
			
		||||
            });
 | 
			
		||||
            const req: ProvidersSamlImportMetadataCreateRequest = {
 | 
			
		||||
                name: name,
 | 
			
		||||
                authorizationFlow: flows.results[0].slug,
 | 
			
		||||
                file: data["metadata"] as Blob,
 | 
			
		||||
            };
 | 
			
		||||
            const provider = await new ProvidersApi(
 | 
			
		||||
                DEFAULT_CONFIG,
 | 
			
		||||
            ).providersSamlImportMetadataCreate(req);
 | 
			
		||||
            this.host.state["provider"] = provider;
 | 
			
		||||
            return true;
 | 
			
		||||
        });
 | 
			
		||||
        return true;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    renderForm(): TemplateResult {
 | 
			
		||||
        return html`<form class="pf-c-form pf-m-horizontal">
 | 
			
		||||
            <ak-form-element-horizontal label=${msg("Metadata")} name="metadata">
 | 
			
		||||
                <input type="file" value="" class="pf-c-form-control" />
 | 
			
		||||
            </ak-form-element-horizontal>
 | 
			
		||||
        </form> `;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										82
									
								
								web/src/admin/applications/wizard/steps.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								web/src/admin/applications/wizard/steps.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,82 @@
 | 
			
		||||
import {
 | 
			
		||||
    BackStep,
 | 
			
		||||
    CancelWizard,
 | 
			
		||||
    CloseWizard,
 | 
			
		||||
    DisabledNextStep,
 | 
			
		||||
    NextStep,
 | 
			
		||||
    SubmitStep,
 | 
			
		||||
} from "@goauthentik/components/ak-wizard-main/commonWizardButtons";
 | 
			
		||||
 | 
			
		||||
import { html } from "lit";
 | 
			
		||||
 | 
			
		||||
import "./application/ak-application-wizard-application-details";
 | 
			
		||||
import "./auth-method-choice/ak-application-wizard-authentication-method-choice";
 | 
			
		||||
import "./commit/ak-application-wizard-commit-application";
 | 
			
		||||
import "./methods/ak-application-wizard-authentication-method";
 | 
			
		||||
import { ApplicationStep as ApplicationStepType } from "./types";
 | 
			
		||||
 | 
			
		||||
class ApplicationStep implements ApplicationStepType {
 | 
			
		||||
    id = "application";
 | 
			
		||||
    label = "Application Details";
 | 
			
		||||
    disabled = false;
 | 
			
		||||
    valid = false;
 | 
			
		||||
    get buttons() {
 | 
			
		||||
        return [this.valid ? NextStep : DisabledNextStep, CancelWizard];
 | 
			
		||||
    }
 | 
			
		||||
    render() {
 | 
			
		||||
        return html`<ak-application-wizard-application-details></ak-application-wizard-application-details>`;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class ProviderMethodStep implements ApplicationStepType {
 | 
			
		||||
    id = "provider-method";
 | 
			
		||||
    label = "Provider Type";
 | 
			
		||||
    disabled = false;
 | 
			
		||||
    valid = false;
 | 
			
		||||
 | 
			
		||||
    get buttons() {
 | 
			
		||||
        return [BackStep, this.valid ? NextStep : DisabledNextStep, CancelWizard];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render() {
 | 
			
		||||
        // prettier-ignore
 | 
			
		||||
        return html`<ak-application-wizard-authentication-method-choice
 | 
			
		||||
          ></ak-application-wizard-authentication-method-choice> `;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class ProviderStepDetails implements ApplicationStepType {
 | 
			
		||||
    id = "provider-details";
 | 
			
		||||
    label = "Provider Configuration";
 | 
			
		||||
    disabled = true;
 | 
			
		||||
    valid = false;
 | 
			
		||||
    get buttons() {
 | 
			
		||||
        return [BackStep, this.valid ? SubmitStep : DisabledNextStep, CancelWizard];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render() {
 | 
			
		||||
        return html`<ak-application-wizard-authentication-method></ak-application-wizard-authentication-method>`;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class SubmitApplicationStep implements ApplicationStepType {
 | 
			
		||||
    id = "submit";
 | 
			
		||||
    label = "Submit Application";
 | 
			
		||||
    disabled = true;
 | 
			
		||||
    valid = false;
 | 
			
		||||
 | 
			
		||||
    get buttons() {
 | 
			
		||||
        return this.valid ? [CloseWizard] : [BackStep, CancelWizard];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render() {
 | 
			
		||||
        return html`<ak-application-wizard-commit-application></ak-application-wizard-commit-application>`;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const newSteps = (): ApplicationStep[] => [
 | 
			
		||||
    new ApplicationStep(),
 | 
			
		||||
    new ProviderMethodStep(),
 | 
			
		||||
    new ProviderStepDetails(),
 | 
			
		||||
    new SubmitApplicationStep(),
 | 
			
		||||
];
 | 
			
		||||
@ -0,0 +1,18 @@
 | 
			
		||||
import { consume } from "@lit-labs/context";
 | 
			
		||||
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
 | 
			
		||||
import { state } from "@lit/reactive-element/decorators/state.js";
 | 
			
		||||
import { LitElement, html } from "lit";
 | 
			
		||||
 | 
			
		||||
import applicationWizardContext from "../ContextIdentity";
 | 
			
		||||
import type { ApplicationWizardState } from "../types";
 | 
			
		||||
 | 
			
		||||
@customElement("ak-application-context-display-for-test")
 | 
			
		||||
export class ApplicationContextDisplayForTest extends LitElement {
 | 
			
		||||
    @consume({ context: applicationWizardContext, subscribe: true })
 | 
			
		||||
    @state()
 | 
			
		||||
    private wizard!: ApplicationWizardState;
 | 
			
		||||
 | 
			
		||||
    render() {
 | 
			
		||||
        return html`<div><pre>${JSON.stringify(this.wizard, null, 2)}</pre></div>`;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,54 @@
 | 
			
		||||
import { Meta } from "@storybook/web-components";
 | 
			
		||||
 | 
			
		||||
import { TemplateResult, html } from "lit";
 | 
			
		||||
 | 
			
		||||
import { ApplicationWizard } from "../ak-application-wizard";
 | 
			
		||||
import "../ak-application-wizard";
 | 
			
		||||
import { mockData } from "./mockData";
 | 
			
		||||
 | 
			
		||||
const metadata: Meta<ApplicationWizard> = {
 | 
			
		||||
    title: "Elements / Application Wizard Implementation / Main",
 | 
			
		||||
    component: "ak-application-wizard",
 | 
			
		||||
    parameters: {
 | 
			
		||||
        docs: {
 | 
			
		||||
            description: {
 | 
			
		||||
                component: "The first page of the application wizard",
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        mockData,
 | 
			
		||||
    },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const LIGHT = "pf-t-light";
 | 
			
		||||
function injectTheme() {
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
        if (!document.body.classList.contains(LIGHT)) {
 | 
			
		||||
            document.body.classList.add(LIGHT);
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default metadata;
 | 
			
		||||
 | 
			
		||||
const container = (testItem: TemplateResult) => {
 | 
			
		||||
    injectTheme();
 | 
			
		||||
    return html` <div style="background: #fff; padding: 1.0rem;">
 | 
			
		||||
        <style>
 | 
			
		||||
            li {
 | 
			
		||||
                display: block;
 | 
			
		||||
            }
 | 
			
		||||
            p {
 | 
			
		||||
                margin-top: 1em;
 | 
			
		||||
            }
 | 
			
		||||
        </style>
 | 
			
		||||
        ${testItem}
 | 
			
		||||
    </div>`;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const MainPage = () => {
 | 
			
		||||
    return container(html`
 | 
			
		||||
        <ak-application-wizard></ak-application-wizard>
 | 
			
		||||
        <hr />
 | 
			
		||||
        <ak-application-context-display-for-test></ak-application-context-display-for-test>
 | 
			
		||||
    `);
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										62
									
								
								web/src/admin/applications/wizard/stories/mockData.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								web/src/admin/applications/wizard/stories/mockData.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,62 @@
 | 
			
		||||
import {
 | 
			
		||||
    dummyAuthenticationFlowsSearch,
 | 
			
		||||
    dummyAuthorizationFlowsSearch,
 | 
			
		||||
    dummyCoreGroupsSearch,
 | 
			
		||||
    dummyCryptoCertsSearch,
 | 
			
		||||
    dummyHasJwks,
 | 
			
		||||
    dummyPropertyMappings,
 | 
			
		||||
    dummyProviderTypesList,
 | 
			
		||||
    dummySAMLProviderMappings,
 | 
			
		||||
} from "./samples";
 | 
			
		||||
 | 
			
		||||
export const mockData = [
 | 
			
		||||
    {
 | 
			
		||||
        url: "/api/v3/providers/all/types/",
 | 
			
		||||
        method: "GET",
 | 
			
		||||
        status: 200,
 | 
			
		||||
        response: dummyProviderTypesList,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        url: "/api/v3/core/groups/?ordering=name",
 | 
			
		||||
        method: "GET",
 | 
			
		||||
        status: 200,
 | 
			
		||||
        response: dummyCoreGroupsSearch,
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    {
 | 
			
		||||
        url: "/api/v3/crypto/certificatekeypairs/?has_key=true&include_details=false&ordering=name",
 | 
			
		||||
        method: "GET",
 | 
			
		||||
        status: 200,
 | 
			
		||||
        response: dummyCryptoCertsSearch,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        url: "/api/v3/flows/instances/?designation=authentication&ordering=slug",
 | 
			
		||||
        method: "GET",
 | 
			
		||||
        status: 200,
 | 
			
		||||
        response: dummyAuthenticationFlowsSearch,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        url: "/api/v3/flows/instances/?designation=authorization&ordering=slug",
 | 
			
		||||
        method: "GET",
 | 
			
		||||
        status: 200,
 | 
			
		||||
        response: dummyAuthorizationFlowsSearch,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        url: "/api/v3/propertymappings/scope/?ordering=scope_name",
 | 
			
		||||
        method: "GET",
 | 
			
		||||
        status: 200,
 | 
			
		||||
        response: dummyPropertyMappings,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        url: "/api/v3/sources/oauth/?has_jwks=true&ordering=name",
 | 
			
		||||
        method: "GET",
 | 
			
		||||
        status: 200,
 | 
			
		||||
        response: dummyHasJwks,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        url: "/api/v3/propertymappings/saml/?ordering=saml_name",
 | 
			
		||||
        method: "GET",
 | 
			
		||||
        status: 200,
 | 
			
		||||
        response: dummySAMLProviderMappings,
 | 
			
		||||
    },
 | 
			
		||||
];
 | 
			
		||||
							
								
								
									
										375
									
								
								web/src/admin/applications/wizard/stories/samples.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										375
									
								
								web/src/admin/applications/wizard/stories/samples.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,375 @@
 | 
			
		||||
export const dummyCryptoCertsSearch = {
 | 
			
		||||
    pagination: {
 | 
			
		||||
        next: 0,
 | 
			
		||||
        previous: 0,
 | 
			
		||||
        count: 1,
 | 
			
		||||
        current: 1,
 | 
			
		||||
        total_pages: 1,
 | 
			
		||||
        start_index: 1,
 | 
			
		||||
        end_index: 1,
 | 
			
		||||
    },
 | 
			
		||||
    results: [
 | 
			
		||||
        {
 | 
			
		||||
            pk: "63efd1b8-6c39-4f65-8157-9a406cb37447",
 | 
			
		||||
            name: "authentik Self-signed Certificate",
 | 
			
		||||
            fingerprint_sha256: null,
 | 
			
		||||
            fingerprint_sha1: null,
 | 
			
		||||
            cert_expiry: null,
 | 
			
		||||
            cert_subject: null,
 | 
			
		||||
            private_key_available: true,
 | 
			
		||||
            private_key_type: null,
 | 
			
		||||
            certificate_download_url:
 | 
			
		||||
                "/api/v3/crypto/certificatekeypairs/63efd1b8-6c39-4f65-8157-9a406cb37447/view_certificate/?download",
 | 
			
		||||
            private_key_download_url:
 | 
			
		||||
                "/api/v3/crypto/certificatekeypairs/63efd1b8-6c39-4f65-8157-9a406cb37447/view_private_key/?download",
 | 
			
		||||
            managed: null,
 | 
			
		||||
        },
 | 
			
		||||
    ],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const dummyAuthenticationFlowsSearch = {
 | 
			
		||||
    pagination: {
 | 
			
		||||
        next: 0,
 | 
			
		||||
        previous: 0,
 | 
			
		||||
        count: 2,
 | 
			
		||||
        current: 1,
 | 
			
		||||
        total_pages: 1,
 | 
			
		||||
        start_index: 1,
 | 
			
		||||
        end_index: 2,
 | 
			
		||||
    },
 | 
			
		||||
    results: [
 | 
			
		||||
        {
 | 
			
		||||
            pk: "2594b1a0-f234-4965-8b93-a8631a55bd5c",
 | 
			
		||||
            policybindingmodel_ptr_id: "0bc529a6-dcd0-4ba8-8fef-5702348832f9",
 | 
			
		||||
            name: "Welcome to authentik!",
 | 
			
		||||
            slug: "default-authentication-flow",
 | 
			
		||||
            title: "Welcome to authentik!",
 | 
			
		||||
            designation: "authentication",
 | 
			
		||||
            background: "/static/dist/assets/images/flow_background.jpg",
 | 
			
		||||
            stages: [
 | 
			
		||||
                "bad9fbce-fb86-4ba4-8124-e7a1d8c147f3",
 | 
			
		||||
                "1da1f272-a76e-4112-be95-f02421fca1d4",
 | 
			
		||||
                "945cd956-6670-4dfa-ab3a-2a72dd3051a7",
 | 
			
		||||
                "0fc1fc5c-b928-4d99-a892-9ae48de089f5",
 | 
			
		||||
            ],
 | 
			
		||||
            policies: [],
 | 
			
		||||
            cache_count: 0,
 | 
			
		||||
            policy_engine_mode: "any",
 | 
			
		||||
            compatibility_mode: false,
 | 
			
		||||
            export_url: "/api/v3/flows/instances/default-authentication-flow/export/",
 | 
			
		||||
            layout: "stacked",
 | 
			
		||||
            denied_action: "message_continue",
 | 
			
		||||
            authentication: "none",
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            pk: "3526dbd1-b50e-4553-bada-fbe7b3c2f660",
 | 
			
		||||
            policybindingmodel_ptr_id: "cde67954-b78a-4fe9-830e-c2aba07a724a",
 | 
			
		||||
            name: "Welcome to authentik!",
 | 
			
		||||
            slug: "default-source-authentication",
 | 
			
		||||
            title: "Welcome to authentik!",
 | 
			
		||||
            designation: "authentication",
 | 
			
		||||
            background: "/static/dist/assets/images/flow_background.jpg",
 | 
			
		||||
            stages: ["3713b252-cee3-4acb-a02f-083f26459fff"],
 | 
			
		||||
            policies: ["f42a4c7f-6586-4b14-9325-a832127ba295"],
 | 
			
		||||
            cache_count: 0,
 | 
			
		||||
            policy_engine_mode: "any",
 | 
			
		||||
            compatibility_mode: false,
 | 
			
		||||
            export_url: "/api/v3/flows/instances/default-source-authentication/export/",
 | 
			
		||||
            layout: "stacked",
 | 
			
		||||
            denied_action: "message_continue",
 | 
			
		||||
            authentication: "require_unauthenticated",
 | 
			
		||||
        },
 | 
			
		||||
    ],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const dummyAuthorizationFlowsSearch = {
 | 
			
		||||
    pagination: {
 | 
			
		||||
        next: 0,
 | 
			
		||||
        previous: 0,
 | 
			
		||||
        count: 2,
 | 
			
		||||
        current: 1,
 | 
			
		||||
        total_pages: 1,
 | 
			
		||||
        start_index: 1,
 | 
			
		||||
        end_index: 2,
 | 
			
		||||
    },
 | 
			
		||||
    results: [
 | 
			
		||||
        {
 | 
			
		||||
            pk: "9e01f011-8b3f-43d6-bedf-c29be5f3a428",
 | 
			
		||||
            policybindingmodel_ptr_id: "14179ef8-2726-4027-9e2f-dc99185199bf",
 | 
			
		||||
            name: "Authorize Application",
 | 
			
		||||
            slug: "default-provider-authorization-explicit-consent",
 | 
			
		||||
            title: "Redirecting to %(app)s",
 | 
			
		||||
            designation: "authorization",
 | 
			
		||||
            background: "/static/dist/assets/images/flow_background.jpg",
 | 
			
		||||
            stages: ["ed5f015f-82b9-450f-addf-1e9d21d8dda3"],
 | 
			
		||||
            policies: [],
 | 
			
		||||
            cache_count: 0,
 | 
			
		||||
            policy_engine_mode: "any",
 | 
			
		||||
            compatibility_mode: false,
 | 
			
		||||
            export_url:
 | 
			
		||||
                "/api/v3/flows/instances/default-provider-authorization-explicit-consent/export/",
 | 
			
		||||
            layout: "stacked",
 | 
			
		||||
            denied_action: "message_continue",
 | 
			
		||||
            authentication: "require_authenticated",
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            pk: "06f11ee3-cbe3-456d-81df-fae4c0a62951",
 | 
			
		||||
            policybindingmodel_ptr_id: "686e6539-8b9f-473e-9f54-e05cc207dd2a",
 | 
			
		||||
            name: "Authorize Application",
 | 
			
		||||
            slug: "default-provider-authorization-implicit-consent",
 | 
			
		||||
            title: "Redirecting to %(app)s",
 | 
			
		||||
            designation: "authorization",
 | 
			
		||||
            background: "/static/dist/assets/images/flow_background.jpg",
 | 
			
		||||
            stages: [],
 | 
			
		||||
            policies: [],
 | 
			
		||||
            cache_count: 0,
 | 
			
		||||
            policy_engine_mode: "any",
 | 
			
		||||
            compatibility_mode: false,
 | 
			
		||||
            export_url:
 | 
			
		||||
                "/api/v3/flows/instances/default-provider-authorization-implicit-consent/export/",
 | 
			
		||||
            layout: "stacked",
 | 
			
		||||
            denied_action: "message_continue",
 | 
			
		||||
            authentication: "require_authenticated",
 | 
			
		||||
        },
 | 
			
		||||
    ],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const dummyCoreGroupsSearch = {
 | 
			
		||||
    pagination: {
 | 
			
		||||
        next: 0,
 | 
			
		||||
        previous: 0,
 | 
			
		||||
        count: 1,
 | 
			
		||||
        current: 1,
 | 
			
		||||
        total_pages: 1,
 | 
			
		||||
        start_index: 1,
 | 
			
		||||
        end_index: 1,
 | 
			
		||||
    },
 | 
			
		||||
    results: [
 | 
			
		||||
        {
 | 
			
		||||
            pk: "67543d37-0ee2-4a4c-b020-9e735a8b5178",
 | 
			
		||||
            num_pk: 13734,
 | 
			
		||||
            name: "authentik Admins",
 | 
			
		||||
            is_superuser: true,
 | 
			
		||||
            parent: null,
 | 
			
		||||
            users: [1],
 | 
			
		||||
            attributes: {},
 | 
			
		||||
            users_obj: [
 | 
			
		||||
                {
 | 
			
		||||
                    pk: 1,
 | 
			
		||||
                    username: "akadmin",
 | 
			
		||||
                    name: "authentik Default Admin",
 | 
			
		||||
                    is_active: true,
 | 
			
		||||
                    last_login: "2023-07-03T16:08:11.196942Z",
 | 
			
		||||
                    email: "ken@goauthentik.io",
 | 
			
		||||
                    attributes: {
 | 
			
		||||
                        settings: {
 | 
			
		||||
                            locale: "en",
 | 
			
		||||
                        },
 | 
			
		||||
                    },
 | 
			
		||||
                    uid: "6dedc98b3fdd0f9afdc705e9d577d61127d89f1d91ea2f90f0b9a353615fb8f2",
 | 
			
		||||
                },
 | 
			
		||||
            ],
 | 
			
		||||
        },
 | 
			
		||||
    ],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const dummyPropertyMappings = {
 | 
			
		||||
    pagination: {
 | 
			
		||||
        next: 0,
 | 
			
		||||
        previous: 0,
 | 
			
		||||
        count: 4,
 | 
			
		||||
        current: 1,
 | 
			
		||||
        total_pages: 1,
 | 
			
		||||
        start_index: 1,
 | 
			
		||||
        end_index: 4,
 | 
			
		||||
    },
 | 
			
		||||
    results: [
 | 
			
		||||
        {
 | 
			
		||||
            pk: "30d87af7-9d9d-4292-873e-a52145ba4bcb",
 | 
			
		||||
            managed: "goauthentik.io/providers/proxy/scope-proxy",
 | 
			
		||||
            name: "authentik default OAuth Mapping: Proxy outpost",
 | 
			
		||||
            expression:
 | 
			
		||||
                '# This mapping is used by the authentik proxy. It passes extra user attributes,\n# which are used for example for the HTTP-Basic Authentication mapping.\nreturn {\n    "ak_proxy": {\n        "user_attributes": request.user.group_attributes(request),\n        "is_superuser": request.user.is_superuser,\n    }\n}',
 | 
			
		||||
            component: "ak-property-mapping-scope-form",
 | 
			
		||||
            verbose_name: "Scope Mapping",
 | 
			
		||||
            verbose_name_plural: "Scope Mappings",
 | 
			
		||||
            meta_model_name: "authentik_providers_oauth2.scopemapping",
 | 
			
		||||
            scope_name: "ak_proxy",
 | 
			
		||||
            description: "authentik Proxy - User information",
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            pk: "3e3751ed-a24c-4f47-a051-e2e05b5cd306",
 | 
			
		||||
            managed: "goauthentik.io/providers/oauth2/scope-email",
 | 
			
		||||
            name: "authentik default OAuth Mapping: OpenID 'email'",
 | 
			
		||||
            expression: 'return {\n    "email": request.user.email,\n    "email_verified": True\n}',
 | 
			
		||||
            component: "ak-property-mapping-scope-form",
 | 
			
		||||
            verbose_name: "Scope Mapping",
 | 
			
		||||
            verbose_name_plural: "Scope Mappings",
 | 
			
		||||
            meta_model_name: "authentik_providers_oauth2.scopemapping",
 | 
			
		||||
            scope_name: "email",
 | 
			
		||||
            description: "Email address",
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            pk: "81c5e330-d8a0-45cd-9cad-e6a49a9c428f",
 | 
			
		||||
            managed: "goauthentik.io/providers/oauth2/scope-openid",
 | 
			
		||||
            name: "authentik default OAuth Mapping: OpenID 'openid'",
 | 
			
		||||
            expression:
 | 
			
		||||
                "# This scope is required by the OpenID-spec, and must as such exist in authentik.\n# The scope by itself does not grant any information\nreturn {}",
 | 
			
		||||
            component: "ak-property-mapping-scope-form",
 | 
			
		||||
            verbose_name: "Scope Mapping",
 | 
			
		||||
            verbose_name_plural: "Scope Mappings",
 | 
			
		||||
            meta_model_name: "authentik_providers_oauth2.scopemapping",
 | 
			
		||||
            scope_name: "openid",
 | 
			
		||||
            description: "",
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            pk: "7ad9cd6f-bcc8-425d-b7c2-c7c4592a1b36",
 | 
			
		||||
            managed: "goauthentik.io/providers/oauth2/scope-profile",
 | 
			
		||||
            name: "authentik default OAuth Mapping: OpenID 'profile'",
 | 
			
		||||
            expression:
 | 
			
		||||
                'return {\n    # Because authentik only saves the user\'s full name, and has no concept of first and last names,\n    # the full name is used as given name.\n    # You can override this behaviour in custom mappings, i.e. `request.user.name.split(" ")`\n    "name": request.user.name,\n    "given_name": request.user.name,\n    "preferred_username": request.user.username,\n    "nickname": request.user.username,\n    # groups is not part of the official userinfo schema, but is a quasi-standard\n    "groups": [group.name for group in request.user.ak_groups.all()],\n}',
 | 
			
		||||
            component: "ak-property-mapping-scope-form",
 | 
			
		||||
            verbose_name: "Scope Mapping",
 | 
			
		||||
            verbose_name_plural: "Scope Mappings",
 | 
			
		||||
            meta_model_name: "authentik_providers_oauth2.scopemapping",
 | 
			
		||||
            scope_name: "profile",
 | 
			
		||||
            description: "General Profile Information",
 | 
			
		||||
        },
 | 
			
		||||
    ],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const dummyHasJwks = {
 | 
			
		||||
    pagination: {
 | 
			
		||||
        next: 0,
 | 
			
		||||
        previous: 0,
 | 
			
		||||
        count: 0,
 | 
			
		||||
        current: 1,
 | 
			
		||||
        total_pages: 1,
 | 
			
		||||
        start_index: 0,
 | 
			
		||||
        end_index: 0,
 | 
			
		||||
    },
 | 
			
		||||
    results: [],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const dummySAMLProviderMappings = {
 | 
			
		||||
    pagination: {
 | 
			
		||||
        next: 0,
 | 
			
		||||
        previous: 0,
 | 
			
		||||
        count: 7,
 | 
			
		||||
        current: 1,
 | 
			
		||||
        total_pages: 1,
 | 
			
		||||
        start_index: 1,
 | 
			
		||||
        end_index: 7,
 | 
			
		||||
    },
 | 
			
		||||
    results: [
 | 
			
		||||
        {
 | 
			
		||||
            pk: "9f1f23b7-1956-4daa-b08b-338cab9b3953",
 | 
			
		||||
            managed: "goauthentik.io/providers/saml/uid",
 | 
			
		||||
            name: "authentik default SAML Mapping: User ID",
 | 
			
		||||
            expression: "return request.user.pk",
 | 
			
		||||
            component: "ak-property-mapping-saml-form",
 | 
			
		||||
            verbose_name: "SAML Property Mapping",
 | 
			
		||||
            verbose_name_plural: "SAML Property Mappings",
 | 
			
		||||
            meta_model_name: "authentik_providers_saml.samlpropertymapping",
 | 
			
		||||
            saml_name: "http://schemas.goauthentik.io/2021/02/saml/uid",
 | 
			
		||||
            friendly_name: null,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            pk: "801b6328-bb0b-4ec6-b52c-f3dc7bb6ec7f",
 | 
			
		||||
            managed: "goauthentik.io/providers/saml/username",
 | 
			
		||||
            name: "authentik default SAML Mapping: Username",
 | 
			
		||||
            expression: "return request.user.username",
 | 
			
		||||
            component: "ak-property-mapping-saml-form",
 | 
			
		||||
            verbose_name: "SAML Property Mapping",
 | 
			
		||||
            verbose_name_plural: "SAML Property Mappings",
 | 
			
		||||
            meta_model_name: "authentik_providers_saml.samlpropertymapping",
 | 
			
		||||
            saml_name: "http://schemas.goauthentik.io/2021/02/saml/username",
 | 
			
		||||
            friendly_name: null,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            pk: "27c4d370-658d-4acf-9f61-cfa6dd020b11",
 | 
			
		||||
            managed: "goauthentik.io/providers/saml/ms-windowsaccountname",
 | 
			
		||||
            name: "authentik default SAML Mapping: WindowsAccountname (Username)",
 | 
			
		||||
            expression: "return request.user.username",
 | 
			
		||||
            component: "ak-property-mapping-saml-form",
 | 
			
		||||
            verbose_name: "SAML Property Mapping",
 | 
			
		||||
            verbose_name_plural: "SAML Property Mappings",
 | 
			
		||||
            meta_model_name: "authentik_providers_saml.samlpropertymapping",
 | 
			
		||||
            saml_name: "http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname",
 | 
			
		||||
            friendly_name: null,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            pk: "757b185b-1c21-42b4-a2ee-04d6f7b655b3",
 | 
			
		||||
            managed: "goauthentik.io/providers/saml/groups",
 | 
			
		||||
            name: "authentik default SAML Mapping: Groups",
 | 
			
		||||
            expression: "for group in request.user.ak_groups.all():\n    yield group.name",
 | 
			
		||||
            component: "ak-property-mapping-saml-form",
 | 
			
		||||
            verbose_name: "SAML Property Mapping",
 | 
			
		||||
            verbose_name_plural: "SAML Property Mappings",
 | 
			
		||||
            meta_model_name: "authentik_providers_saml.samlpropertymapping",
 | 
			
		||||
            saml_name: "http://schemas.xmlsoap.org/claims/Group",
 | 
			
		||||
            friendly_name: null,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            pk: "de67cee7-7c56-4c1d-9466-9ad0e0105092",
 | 
			
		||||
            managed: "goauthentik.io/providers/saml/email",
 | 
			
		||||
            name: "authentik default SAML Mapping: Email",
 | 
			
		||||
            expression: "return request.user.email",
 | 
			
		||||
            component: "ak-property-mapping-saml-form",
 | 
			
		||||
            verbose_name: "SAML Property Mapping",
 | 
			
		||||
            verbose_name_plural: "SAML Property Mappings",
 | 
			
		||||
            meta_model_name: "authentik_providers_saml.samlpropertymapping",
 | 
			
		||||
            saml_name: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
 | 
			
		||||
            friendly_name: null,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            pk: "42a936a5-11a9-4442-8748-ec27a8ab9546",
 | 
			
		||||
            managed: "goauthentik.io/providers/saml/name",
 | 
			
		||||
            name: "authentik default SAML Mapping: Name",
 | 
			
		||||
            expression: "return request.user.name",
 | 
			
		||||
            component: "ak-property-mapping-saml-form",
 | 
			
		||||
            verbose_name: "SAML Property Mapping",
 | 
			
		||||
            verbose_name_plural: "SAML Property Mappings",
 | 
			
		||||
            meta_model_name: "authentik_providers_saml.samlpropertymapping",
 | 
			
		||||
            saml_name: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name",
 | 
			
		||||
            friendly_name: null,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            pk: "06bee8f0-e5b4-4ce8-959a-308ba0769917",
 | 
			
		||||
            managed: "goauthentik.io/providers/saml/upn",
 | 
			
		||||
            name: "authentik default SAML Mapping: UPN",
 | 
			
		||||
            expression: "return request.user.attributes.get('upn', request.user.email)",
 | 
			
		||||
            component: "ak-property-mapping-saml-form",
 | 
			
		||||
            verbose_name: "SAML Property Mapping",
 | 
			
		||||
            verbose_name_plural: "SAML Property Mappings",
 | 
			
		||||
            meta_model_name: "authentik_providers_saml.samlpropertymapping",
 | 
			
		||||
            saml_name: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn",
 | 
			
		||||
            friendly_name: null,
 | 
			
		||||
        },
 | 
			
		||||
    ],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// prettier-ignore
 | 
			
		||||
export const dummyProviderTypesList = [
 | 
			
		||||
    ["LDAP Provider", "ldapprovider", 
 | 
			
		||||
     "Allow applications to authenticate against authentik's users using LDAP.", 
 | 
			
		||||
    ], 
 | 
			
		||||
    ["OAuth2/OpenID Provider", "oauth2provider", 
 | 
			
		||||
     "OAuth2 Provider for generic OAuth and OpenID Connect Applications.", 
 | 
			
		||||
    ], 
 | 
			
		||||
    ["Proxy Provider", "proxyprovider", 
 | 
			
		||||
     "Protect applications that don't support any of the other\n    Protocols by using a Reverse-Proxy.", 
 | 
			
		||||
    ], 
 | 
			
		||||
    ["Radius Provider", "radiusprovider", 
 | 
			
		||||
     "Allow applications to authenticate against authentik's users using Radius.", 
 | 
			
		||||
    ], 
 | 
			
		||||
    ["SAML Provider", "samlprovider", 
 | 
			
		||||
     "SAML 2.0 Endpoint for applications which support SAML.", 
 | 
			
		||||
    ], 
 | 
			
		||||
    ["SCIM Provider", "scimprovider", 
 | 
			
		||||
     "SCIM 2.0 provider to create users and groups in external applications", 
 | 
			
		||||
    ], 
 | 
			
		||||
    ["SAML Provider from Metadata", "", 
 | 
			
		||||
     "Create a SAML Provider by importing its Metadata.", 
 | 
			
		||||
    ], 
 | 
			
		||||
].map(([name, model_name, description]) => ({ name, description, model_name }));
 | 
			
		||||
							
								
								
									
										39
									
								
								web/src/admin/applications/wizard/types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								web/src/admin/applications/wizard/types.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,39 @@
 | 
			
		||||
import { type WizardStep } from "@goauthentik/components/ak-wizard-main/types";
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
    type ApplicationRequest,
 | 
			
		||||
    type LDAPProviderRequest,
 | 
			
		||||
    type OAuth2ProviderRequest,
 | 
			
		||||
    type ProvidersSamlImportMetadataCreateRequest,
 | 
			
		||||
    type ProxyProviderRequest,
 | 
			
		||||
    type RadiusProviderRequest,
 | 
			
		||||
    type SAMLProviderRequest,
 | 
			
		||||
    type SCIMProviderRequest,
 | 
			
		||||
} from "@goauthentik/api";
 | 
			
		||||
 | 
			
		||||
export type OneOfProvider =
 | 
			
		||||
    | Partial<SCIMProviderRequest>
 | 
			
		||||
    | Partial<SAMLProviderRequest>
 | 
			
		||||
    | Partial<ProvidersSamlImportMetadataCreateRequest>
 | 
			
		||||
    | Partial<RadiusProviderRequest>
 | 
			
		||||
    | Partial<ProxyProviderRequest>
 | 
			
		||||
    | Partial<OAuth2ProviderRequest>
 | 
			
		||||
    | Partial<LDAPProviderRequest>;
 | 
			
		||||
 | 
			
		||||
export interface ApplicationWizardState {
 | 
			
		||||
    providerModel: string;
 | 
			
		||||
    app: Partial<ApplicationRequest>;
 | 
			
		||||
    provider: OneOfProvider;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type StatusType = "invalid" | "valid" | "submitted" | "failed";
 | 
			
		||||
 | 
			
		||||
export type ApplicationWizardStateUpdate = {
 | 
			
		||||
    update?: Partial<ApplicationWizardState>;
 | 
			
		||||
    status?: StatusType;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type ApplicationStep = WizardStep & {
 | 
			
		||||
    id: string;
 | 
			
		||||
    valid: boolean;
 | 
			
		||||
};
 | 
			
		||||
		Reference in New Issue
	
	Block a user