import * as plugins from '../plugins.js'; import { customElement, DeesElement, property, html, type TemplateResult, css, cssManager, state, domtools, } from '@design.estate/dees-element'; import '@uptime.link/webwidget'; import '@design.estate/dees-catalog'; import { DeesForm, DeesFormSubmit, DeesInputText } from '@design.estate/dees-catalog'; import { IdpState } from '../states/idp.state.js'; declare global { interface HTMLElementTagNameMap { 'idp-loginprompt': IdpLoginPrompt; } } @customElement('idp-loginprompt') export class IdpLoginPrompt extends DeesElement { public static demo = () => html``; @state() accessor oidcConsentState: plugins.idpInterfaces.request.IReq_PrepareOidcAuthorization['response'] | null = null; @state() accessor oidcConsentError = ''; @property() accessor productOfInterest: string; @property() accessor jwt: string; @property({ reflect: true, type: Object, }) accessor appData: plugins.idpInterfaces.data.IApp; public jwtObserable = new domtools.plugins.smartrx.rxjs.Subject(); constructor() { super(); domtools.elementBasic.setup(); } private getOidcAuthorizationContext(): Omit< plugins.idpInterfaces.request.IReq_CompleteOidcAuthorization['request'], 'jwt' > | null { const currentUrl = plugins.smarturl.Smarturl.createFromUrl(window.location.href); if (currentUrl.searchParams.oauth !== 'true') { return null; } const clientId = currentUrl.searchParams.client_id; const redirectUri = currentUrl.searchParams.redirect_uri; const scope = currentUrl.searchParams.scope; const state = currentUrl.searchParams.state; if (!clientId || !redirectUri || !scope || !state) { return null; } const prompt = ['none', 'login', 'consent'].includes(currentUrl.searchParams.prompt) ? (currentUrl.searchParams.prompt as 'none' | 'login' | 'consent') : undefined; return { clientId, redirectUri, scope, state, prompt, codeChallenge: currentUrl.searchParams.code_challenge || undefined, codeChallengeMethod: currentUrl.searchParams.code_challenge_method === 'S256' ? 'S256' : undefined, nonce: currentUrl.searchParams.nonce || undefined, }; } private redirectOidcError(errorArg: string, descriptionArg?: string) { const oidcContext = this.getOidcAuthorizationContext(); if (!oidcContext) { return false; } const redirectUrl = new URL(oidcContext.redirectUri); redirectUrl.searchParams.set('error', errorArg); redirectUrl.searchParams.set('state', oidcContext.state); if (descriptionArg) { redirectUrl.searchParams.set('error_description', descriptionArg); } window.location.href = redirectUrl.toString(); return true; } private getOidcScopeDescription(scopeArg: plugins.idpInterfaces.data.TOidcScope) { const scopeMap: Record = { openid: 'Confirm your identity with this app.', profile: 'Share your display name and username.', email: 'Share your email address.', organizations: 'Share your organizations and their roles.', roles: 'Share your platform roles.', }; return scopeMap[scopeArg]; } private getOidcAppHost(appUrlArg: string) { try { return new URL(appUrlArg).hostname; } catch { return appUrlArg; } } private async prepareOidcAuthorization(jwtArg: string) { const oidcContext = this.getOidcAuthorizationContext(); if (!oidcContext) { return null; } const idpState = await IdpState.getSingletonInstance(); return idpState.idpClient.requests.prepareOidcAuthorization .fire({ jwt: jwtArg, ...oidcContext, }) .catch(() => null); } private async handleOidcAfterLogin(jwtArg: string) { const oidcContext = this.getOidcAuthorizationContext(); if (!oidcContext) { return false; } const loginForm = this.shadowRoot.querySelector('#loginForm') as DeesForm | null; loginForm?.setStatus('pending', 'preparing application authorization...'); this.oidcConsentError = ''; const preparation = await this.prepareOidcAuthorization(jwtArg); if (!preparation) { loginForm?.setStatus('error', 'could not prepare the application authorization'); return true; } if (preparation.status === 'consent_required') { if (oidcContext.prompt === 'none') { this.redirectOidcError('consent_required'); return true; } this.oidcConsentState = preparation; return true; } await this.completeOidcAuthorization(jwtArg); return true; } private async completeOidcAuthorization(jwtArg: string, consentApproved = false) { const oidcContext = this.getOidcAuthorizationContext(); if (!oidcContext) { return false; } const idpState = await IdpState.getSingletonInstance(); const loginForm = this.shadowRoot.querySelector('#loginForm') as DeesForm | null; loginForm?.setStatus('pending', 'authorizing application...'); this.oidcConsentError = ''; const response = await idpState.idpClient.requests.completeOidcAuthorization .fire({ jwt: jwtArg, ...oidcContext, consentApproved, }) .catch(() => null); if (!response?.redirectUrl) { if (this.oidcConsentState) { this.oidcConsentError = 'Could not authorize the application.'; } else { loginForm?.setStatus('error', 'could not authorize the application'); } return false; } window.location.href = response.redirectUrl; return true; } public static styles = [ cssManager.defaultStyles, css` :host { --foreground: hsl(0 0% 98%); --muted-foreground: hsl(240 5% 64.9%); font-family: 'Geist Sans', -apple-system, BlinkMacSystemFont, sans-serif; display: block; color: var(--foreground); } .form-header { margin-bottom: 32px; text-align: center; } .form-header h2 { font-size: 24px; font-weight: 600; color: var(--foreground); margin: 0 0 8px 0; letter-spacing: -0.02em; } .form-header p { font-size: 14px; color: var(--muted-foreground); margin: 0; } dees-form { display: flex; flex-direction: column; gap: 16px; } .form-footer { margin-top: 24px; text-align: center; font-size: 14px; color: var(--muted-foreground); } .form-footer a { color: var(--foreground); text-decoration: none; font-weight: 500; cursor: pointer; transition: opacity 0.15s ease; } .form-footer a:hover { opacity: 0.8; } .consent-card { display: flex; flex-direction: column; gap: 16px; padding: 24px; border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 18px; background: rgba(255, 255, 255, 0.04); } .consent-appname { font-size: 20px; font-weight: 600; } .consent-appurl { color: var(--muted-foreground); font-size: 14px; word-break: break-word; } .consent-scopes { display: flex; flex-direction: column; gap: 12px; } .consent-scope { padding: 14px 16px; border-radius: 14px; background: rgba(255, 255, 255, 0.03); } .consent-scope-header { display: flex; justify-content: space-between; gap: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; font-size: 12px; } .consent-scope-tag { color: #9cd67c; } .consent-scope-description { margin-top: 6px; color: var(--muted-foreground); font-size: 14px; line-height: 1.5; } .consent-actions { display: flex; justify-content: flex-end; gap: 12px; } .consent-button { border: none; border-radius: 999px; padding: 12px 18px; font: inherit; cursor: pointer; } .consent-button-secondary { background: rgba(255, 255, 255, 0.08); color: var(--foreground); } .consent-button-primary { background: linear-gradient(135deg, #9b7bff, #5fd1ff); color: #0a0a0a; font-weight: 600; } .consent-error { color: #ff9a9a; font-size: 14px; } `, ]; public render(): TemplateResult { if (this.oidcConsentState) { return html`

Continue to ${this.oidcConsentState.appName}

Review and approve the access this app is requesting.

`; } return html`

Sign in to your account

Enter your credentials to continue

{ this.login({ emailAddress: eventArg.detail.data.emailAddress, passwordArg: eventArg.detail.data.password, }); }} >
`; } public async firstUpdated() { await this.domtoolsPromise; const idpState = await IdpState.getSingletonInstance(); const loginForm = this.shadowRoot.querySelector('#loginForm') as DeesForm; const loginPasswordInput = loginForm.querySelector('#loginPasswordInput') as DeesInputText; const loginSubmitButton = loginForm.querySelector('#loginSubmitButton') as DeesFormSubmit; const oidcContext = this.getOidcAuthorizationContext(); const setButtonText = async () => { if (loginPasswordInput.value) { loginSubmitButton.text = oidcContext ? 'Sign in and continue' : 'Login'; } else { loginSubmitButton.text = 'Send magic link (or enter password)'; } }; loginForm.changeSubject.subscribe(() => { void setButtonText(); }); await setButtonText(); if (oidcContext) { const loggedIn = await idpState.idpClient.determineLoginStatus(false); if (!loggedIn && oidcContext.prompt === 'none') { this.redirectOidcError('login_required'); return; } if (loggedIn && oidcContext.prompt !== 'login') { const jwt = await idpState.idpClient.getJwt(); if (jwt) { await this.handleOidcAfterLogin(jwt); } } } } private login = async (valueArg: { emailAddress: string; passwordArg: string }) => { const loginSubmitButton = this.shadowRoot.querySelector( '#loginSubmitButton' ) as plugins.deesCatalog.DeesFormSubmit; loginSubmitButton.disabled = true; const idpState = await IdpState.getSingletonInstance(); const loginForm = this.shadowRoot.querySelector('#loginForm') as DeesForm; const loginRequestWithUsernameAndPassword = idpState.idpClient.typedsocket.createTypedRequest( 'loginWithEmailOrUsernameAndPassword' ); const loginRequestWithEmail = idpState.idpClient.typedsocket.createTypedRequest( 'loginWithEmail' ); if (valueArg.emailAddress && valueArg.passwordArg) { loginForm.setStatus('pending', 'logging in...'); const response = await loginRequestWithUsernameAndPassword .fire({ username: valueArg.emailAddress, password: valueArg.passwordArg, }) .catch(() => { loginForm.setStatus('error', 'could not log you in. Try Again!'); return null; }); if (!response) { loginSubmitButton.disabled = false; return; } if (response.refreshToken) { loginForm.setStatus('pending', 'obtained refreshToken...'); const jwt = await idpState.idpClient.refreshJwt(response.refreshToken); if (jwt) { loginForm.setStatus('success', 'obtained jwt.'); const oidcHandled = await this.handleOidcAfterLogin(jwt); if (!oidcHandled) { idpState.domtools.router.pushUrl('/account'); } } else { loginForm.setStatus('error', 'something went wrong'); } } } else if (valueArg.emailAddress && !valueArg.passwordArg) { loginForm.setStatus('pending', 'sending magic link...'); const response = await loginRequestWithEmail.fire({ email: valueArg.emailAddress, }); if (response.status === 'ok') { loginForm.setStatus('success', 'Please check your email!'); } } loginSubmitButton.disabled = false; }; public async dispatchJwt(jwtArg?: string) { if (jwtArg !== undefined) { this.jwt = jwtArg; await domtools.plugins.smartdelay.delayFor(200); this.dispatchEvent( new CustomEvent('leleLoginGotJwt', { detail: { jwt: this.jwt, }, }) ); this.jwtObserable.next(this.jwt); } } public async focus() { (this.shadowRoot.querySelector('#loginEmailInput') as plugins.deesCatalog.DeesInputText).focus(); } public async show() { await this.updateComplete; const centerContainer = this.shadowRoot.querySelector('idp-centercontainer'); await centerContainer.show(); } public async hide() { await this.updateComplete; const centerContainer = this.shadowRoot.querySelector('idp-centercontainer'); await centerContainer.hide(); } }