feat: implement automation feature
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Global SSO auth state for the current org. Shared by login and signup
|
||||
* so we don't re-fetch when navigating between them.
|
||||
*/
|
||||
|
||||
import { get, writable } from 'svelte/store';
|
||||
import { INITIAL_SSO_AUTH_STATE, ssoStateFromOrgInfo, type OrgSsoInfoShape, type SsoAuthState } from './auth-sso';
|
||||
|
||||
export type AuthSsoStore = {
|
||||
/** Org we loaded SSO info for; when nav changes org we re-fetch */
|
||||
loadedForOrgId: string | null;
|
||||
/** Org-level SSO state from getOrgSsoInfo */
|
||||
ssoState: SsoAuthState;
|
||||
orgSupportsSso: boolean;
|
||||
};
|
||||
|
||||
const initial: AuthSsoStore = {
|
||||
loadedForOrgId: null,
|
||||
ssoState: INITIAL_SSO_AUTH_STATE,
|
||||
orgSupportsSso: false
|
||||
};
|
||||
|
||||
export const authSsoStore = writable<AuthSsoStore>(initial);
|
||||
|
||||
/**
|
||||
* Ensures SSO info for the given org is loaded. If we already have data for
|
||||
* this orgId, does nothing. Otherwise fetches and updates the store.
|
||||
*/
|
||||
export async function ensureSsoInfoLoaded(
|
||||
orgId: string,
|
||||
fetchOrgSsoInfo: (id: string) => Promise<OrgSsoInfoShape>
|
||||
): Promise<void> {
|
||||
const current = get(authSsoStore);
|
||||
if (current.loadedForOrgId === orgId) return;
|
||||
|
||||
const orgInfo = await fetchOrgSsoInfo(orgId);
|
||||
const ssoState = ssoStateFromOrgInfo(orgInfo);
|
||||
|
||||
authSsoStore.set({
|
||||
loadedForOrgId: orgId,
|
||||
ssoState,
|
||||
orgSupportsSso: orgInfo?.hasSso ?? false
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* Shared SSO auth state and helpers for login/signup flows.
|
||||
* Used by (auth)/login and (auth)/signup to avoid duplicating SSO logic.
|
||||
*/
|
||||
|
||||
export type SsoAuthState = {
|
||||
checking: boolean;
|
||||
required: boolean;
|
||||
available: boolean;
|
||||
redirectUrl: string | null;
|
||||
providerName: string | null;
|
||||
providerId: string | null;
|
||||
};
|
||||
|
||||
export const INITIAL_SSO_AUTH_STATE: SsoAuthState = {
|
||||
checking: false,
|
||||
required: false,
|
||||
available: false,
|
||||
redirectUrl: null,
|
||||
providerName: null,
|
||||
providerId: null
|
||||
};
|
||||
|
||||
/** Shape returned by getOrgSsoInfo (avoids importing org types here). */
|
||||
export type OrgSsoInfoShape = {
|
||||
hasSso: boolean;
|
||||
ssoRequired?: boolean;
|
||||
redirectUrl?: string | null;
|
||||
providerName?: string | null;
|
||||
providerId?: string | null;
|
||||
} | null;
|
||||
|
||||
/** Shape returned by discoverSso (avoids importing org types here). */
|
||||
export type SsoDiscoveryResultShape = {
|
||||
ssoRequired?: boolean;
|
||||
ssoAvailable?: boolean;
|
||||
redirectUrl?: string | null;
|
||||
providerName?: string | null;
|
||||
providerId?: string | null;
|
||||
} | null;
|
||||
|
||||
export function ssoStateFromOrgInfo(orgInfo: OrgSsoInfoShape): SsoAuthState {
|
||||
if (!orgInfo) return INITIAL_SSO_AUTH_STATE;
|
||||
return {
|
||||
checking: false,
|
||||
required: orgInfo.ssoRequired ?? false,
|
||||
available: orgInfo.hasSso ?? false,
|
||||
redirectUrl: orgInfo.redirectUrl ?? null,
|
||||
providerName: orgInfo.providerName ?? null,
|
||||
providerId: orgInfo.providerId ?? null
|
||||
};
|
||||
}
|
||||
|
||||
export function ssoStateFromDiscovery(result: SsoDiscoveryResultShape): SsoAuthState {
|
||||
if (!result) return INITIAL_SSO_AUTH_STATE;
|
||||
return {
|
||||
checking: false,
|
||||
required: result.ssoRequired ?? false,
|
||||
available: result.ssoAvailable ?? false,
|
||||
redirectUrl: result.redirectUrl ?? null,
|
||||
providerName: result.providerName ?? null,
|
||||
providerId: result.providerId ?? null
|
||||
};
|
||||
}
|
||||
|
||||
export function buildSsoRedirectUrl(redirectParam: string | null): string {
|
||||
const base = typeof window !== 'undefined' ? window.location.origin : '';
|
||||
const path = redirectParam && redirectParam.startsWith('/') ? redirectParam : '';
|
||||
return `${base}${path}`;
|
||||
}
|
||||
|
||||
const SSO_EMAIL_DEBOUNCE_MS = 500;
|
||||
|
||||
export function createSsoEmailChecker(options: {
|
||||
getEmail: () => string;
|
||||
getOrgSupportsSso: () => boolean;
|
||||
discoverSso: (email: string) => Promise<SsoDiscoveryResultShape>;
|
||||
onChecking: () => void;
|
||||
onResult: (state: SsoAuthState) => void;
|
||||
onClear: () => void;
|
||||
}) {
|
||||
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
return function handleEmailChange() {
|
||||
options.onClear();
|
||||
if (!options.getOrgSupportsSso()) return;
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
const email = options.getEmail();
|
||||
if (!email || !email.includes('@')) {
|
||||
options.onResult(INITIAL_SSO_AUTH_STATE);
|
||||
return;
|
||||
}
|
||||
|
||||
options.onChecking();
|
||||
timeoutId = setTimeout(async () => {
|
||||
const result = await options.discoverSso(email);
|
||||
options.onResult(ssoStateFromDiscovery(result));
|
||||
}, SSO_EMAIL_DEBOUNCE_MS);
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user