vault/ui/app/components/auth/form-template.ts
claire bontempo 31051ef1e4
UI: Implement api service in auth components (#31085)
* change entity_id to camel casing, remove "backends" key from stored auth data

* fix tokenExpirationEpoch returning NaN, use authSuccess in auth service tests

* camel case mfa_requirement references

* refactor auth service

* implement api service for token method

* implement api service in standard auth methods

* add lookupSelf request to persistAuthData method in auht service instead of calling in components

* implement api service in oidc-jwt component

* implement api service in okta component

* implement api service in saml component

* use api service for wrapped_token query param

* remaining test updates, enterprise tests and stabilize auth helpers

* upate renew() to use new persistAuthData method, add a test

* revert as this will be addressed upstream

* rename supported-login-methods to auth-form-helpers and delete old supported-auth-backends helper, update tests

* cleanup normalize after testing mfa validation for each auth method

* update type declarations, set displayName in each method component

* stabilize redirect tests by waiting for login before asserting url

* stabilize tests

* modernize typescript syntax, move error const to util

* use mirage instead of vault server to resolve test race conditions

* fix file import
2025-07-09 10:11:23 -07:00

119 lines
4.8 KiB
TypeScript

/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@glimmer/component';
import { service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { supportedTypes } from 'vault/utils/auth-form-helpers';
import type VersionService from 'vault/services/version';
import type ClusterModel from 'vault/models/cluster';
import type { UnauthMountsByType } from 'vault/vault/auth/form';
import type { HTMLElementEvent } from 'vault/forms';
/**
* @module Auth::FormTemplate
* This component is responsible for managing the layout and display logic for the auth form. When initialized it fetches
* the unauthenticated sys/internal/ui/mounts endpoint to check the listing_visibility configuration of available mounts.
* If mounts have been configured as listing_visibility="unauth" then tabs render for the corresponding method types,
* otherwise all auth methods display in a dropdown list. The endpoint is re-requested anytime the namespace input is updated.
*
* When auth type changes (by selecting a new one from the dropdown or select a tab), the form component updates and
* dynamically renders the corresponding form.
*
*
* @param {object | null} alternateView - if an alternate view exists, this is the `FormView` (see interface below) data to render that view.
* @param {string} canceledMfaAuth - saved auth type from a cancelled mfa verification
* @param {object} cluster - The route model which is the ember data cluster model. contains information such as cluster id, name and boolean for if the cluster is in standby
* @param {object} defaultView - The `FormView` (see the interface below) data to render the initial view.
* @param {object} initialFormState - sets selectedAuthMethod and showAlternateView based on the login form configuration computed in parent component
* @param {function} onSuccess - callback after the initial authentication request, if an mfaRequirement exists the parent renders the mfa form otherwise it fires the authSuccess action in the auth controller and handles transitioning to the app
* @param {array} visibleMountTypes - array of auth method types that have mounts with listing_visibility="unauth"
* */
interface Args {
alternateView: FormView | null;
cluster: ClusterModel;
defaultView: FormView;
initialFormState: { initialAuthType: string; showAlternate: boolean };
onSuccess: CallableFunction;
visibleMountTypes: string[];
}
interface FormView {
view: string; // "dropdown" or "tabs"
tabData: UnauthMountsByType | null; // tabs to render if view = "tabs"
}
export default class AuthFormTemplate extends Component<Args> {
@service declare readonly version: VersionService;
supportedAuthTypes: string[];
@tracked errorMessage = '';
@tracked selectedAuthMethod = '';
// true → "Back" button renders, false → "Sign in with other methods→" renders if an alternate view exists
@tracked showAlternateView = false;
constructor(owner: unknown, args: Args) {
super(owner, args);
const { initialAuthType, showAlternate } = this.args.initialFormState;
this.selectedAuthMethod = initialAuthType;
this.showAlternateView = showAlternate;
this.supportedAuthTypes = supportedTypes(this.version.isEnterprise);
}
get tabData() {
if (this.showAlternateView) return this.args?.alternateView?.tabData;
return this.args?.defaultView?.tabData;
}
get formComponent() {
const { selectedAuthMethod } = this;
// isSupported means there is a component file defined for that auth type
const isSupported = this.supportedAuthTypes.includes(selectedAuthMethod);
const formFile = () => (['oidc', 'jwt'].includes(selectedAuthMethod) ? 'oidc-jwt' : selectedAuthMethod);
const component = isSupported ? formFile() : 'base';
// an Auth::Form::<Type> component exists for each method in supported-login-methods
return `auth/form/${component}`;
}
get hideAdvancedSettings() {
// Token does not support custom paths
if (this.selectedAuthMethod === 'token') return true;
// Always show for dropdown mode
if (!this.tabData) return false;
// For remaining scenarios, hide "Advanced settings" if the selected method has visible mount(s)
return this.args.visibleMountTypes?.includes(this.selectedAuthMethod);
}
@action
setAuthType(authType: string) {
this.selectedAuthMethod = authType;
}
@action
setTypeFromDropdown(event: HTMLElementEvent<HTMLInputElement>) {
this.selectedAuthMethod = event.target.value;
}
@action
toggleView() {
this.showAlternateView = !this.showAlternateView;
const firstAuthTab = Object.keys(this.tabData || {})[0];
const type = firstAuthTab || this.args.initialFormState.initialAuthType;
this.setAuthType(type);
}
@action
handleError(message: string) {
this.errorMessage = message;
}
}