@petrarca/sonnet-shell 0.4.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/index.d.ts +42 -30
- package/dist/auth/index.js +412 -119
- package/dist/auth/index.js.map +1 -1
- package/dist/index.d.ts +14 -0
- package/dist/index.js +15 -9
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/auth/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/auth/Login.tsx","../../src/auth/TenantSelection.tsx","../../src/auth/ProtectedRoute.tsx"],"sourcesContent":["import { useState, useEffect, useCallback } from \"react\";\nimport { useNavigate, useLocation } from \"react-router-dom\";\nimport { Button } from \"@petrarca/sonnet-ui\";\nimport { Input } from \"@petrarca/sonnet-ui\";\nimport { Label } from \"@petrarca/sonnet-ui\";\nimport { Alert } from \"@petrarca/sonnet-ui\";\nimport { TenantSelection, type TenantOption } from \"./TenantSelection\";\nimport { normalizeError } from \"@petrarca/sonnet-core\";\n\n/**\n * Login -- multi-step authentication flow.\n *\n * Step 1: Credentials (local auth form or OIDC redirect)\n * Step 2: Tenant selection (if user has multiple tenants and no default)\n *\n * Steps are shown conditionally. If the user has a default tenant,\n * step 2 is skipped and the app loads immediately.\n *\n * ProtectedRoute passes a `step` hint in location.state so this\n * component always opens on the correct step when redirected.\n *\n * All auth state and operations are injected via props -- no direct\n * store or context imports.\n * Target package: @petrarca/sonnet-shell\n */\n\n/** Auth feature flags and provider configuration. Consumer-supplied. */\nexport interface AuthConfig {\n enabled: boolean;\n features: {\n local_auth: boolean;\n oauth: boolean;\n };\n oauth?: {\n providers: Record<\n string,\n {\n enabled: boolean;\n name: string;\n login_url: string;\n }\n >;\n } | null;\n ui: {\n direct_redirect: string | null;\n };\n}\n\ntype LoginStep = \"credentials\" | \"tenant-selection\";\n\ninterface LocationState {\n from?: { pathname: string };\n step?: LoginStep;\n}\n\nexport interface LoginProps {\n /** Auth feature flags and OAuth provider config. */\n config: AuthConfig | null;\n /** Whether the user is currently authenticated (token present). */\n isAuthenticated: boolean;\n /** Active tenant id, null when not yet selected. */\n activeTenantId: string | null;\n /** Whether an async operation is in flight. */\n isLoading: boolean;\n /** Auth error message to display, null when none. */\n error: string | null;\n /** The authenticated user's available tenants. */\n tenants: TenantOption[] | null;\n /** Perform credential login. Throws on failure. */\n onLogin: (credentials: {\n identifier: string;\n password: string;\n }) => Promise<void>;\n /** Fetch the current user after login to resolve tenant state. */\n onFetchCurrentUser: () => Promise<void>;\n /** Switch to the given tenant. Throws on failure. */\n onSwitchTenant: (id: string) => Promise<void>;\n /** Clear the current auth error. */\n onClearError: () => void;\n}\n\n// Custom hook: manages the login flow state (steps, redirects, tenant resolution).\nfunction useLoginFlow(props: LoginProps) {\n const {\n config,\n isAuthenticated,\n activeTenantId,\n tenants,\n onLogin,\n onFetchCurrentUser,\n onSwitchTenant,\n onClearError,\n } = props;\n const navigate = useNavigate();\n const location = useLocation();\n const locationState = location.state as LocationState | null;\n\n const [redirectTo] = useState(() => locationState?.from?.pathname ?? \"/\");\n const [step, setStep] = useState<LoginStep>(\n () => locationState?.step ?? \"credentials\",\n );\n const [identifier, setIdentifier] = useState(\"\");\n const [password, setPassword] = useState(\"\");\n const [tenantError, setTenantError] = useState<string | null>(null);\n\n useEffect(() => {\n if (isAuthenticated && activeTenantId)\n navigate(redirectTo, { replace: true });\n }, [isAuthenticated, activeTenantId, navigate, redirectTo]);\n\n useEffect(() => {\n if (!isAuthenticated || !tenants || activeTenantId) return;\n if (tenants.length === 1) {\n onSwitchTenant(tenants[0].id).catch((err) => {\n setTenantError(\n normalizeError(err).message || \"Failed to select tenant\",\n );\n setStep(\"tenant-selection\");\n });\n } else if (tenants.length > 1) {\n setStep(\"tenant-selection\");\n }\n }, [isAuthenticated, tenants, activeTenantId, onSwitchTenant]);\n\n useEffect(() => {\n if (config?.ui.direct_redirect)\n window.location.href = config.ui.direct_redirect;\n }, [config]);\n\n const handleCredentialSubmit = useCallback(\n async (e: React.FormEvent) => {\n e.preventDefault();\n onClearError();\n try {\n await onLogin({ identifier, password });\n await onFetchCurrentUser();\n } catch {\n /* error via prop */\n }\n },\n [identifier, password, onLogin, onFetchCurrentUser, onClearError],\n );\n\n const handleTenantSelect = useCallback(\n async (id: string) => {\n setTenantError(null);\n try {\n await onSwitchTenant(id);\n } catch (err) {\n setTenantError(\n normalizeError(err).message || \"Failed to select tenant\",\n );\n }\n },\n [onSwitchTenant],\n );\n\n return {\n step,\n identifier,\n setIdentifier,\n password,\n setPassword,\n tenantError,\n handleCredentialSubmit,\n handleTenantSelect,\n };\n}\n\nexport function Login(props: LoginProps): React.ReactElement | null {\n const { config, isLoading, error, tenants } = props;\n const flow = useLoginFlow(props);\n\n if (config?.ui.direct_redirect) {\n return <RedirectingMessage />;\n }\n\n if (flow.step === \"tenant-selection\" && tenants) {\n return (\n <TenantSelection\n tenants={tenants}\n isLoading={isLoading}\n error={flow.tenantError}\n onSelect={flow.handleTenantSelect}\n />\n );\n }\n\n return (\n <CredentialPage\n config={config}\n error={error}\n isLoading={isLoading}\n flow={flow}\n />\n );\n}\n\nfunction RedirectingMessage() {\n return (\n <div className=\"flex items-center justify-center min-h-screen\">\n <p className=\"text-muted-foreground\">Redirecting to authentication...</p>\n </div>\n );\n}\n\ninterface CredentialPageProps {\n config: AuthConfig | null;\n error: string | null;\n isLoading: boolean;\n flow: ReturnType<typeof useLoginFlow>;\n}\n\nfunction CredentialPage({\n config,\n error,\n isLoading,\n flow,\n}: CredentialPageProps) {\n return (\n <div className=\"flex items-center justify-center min-h-screen bg-slate-100\">\n <div className=\"w-full max-w-md bg-white rounded-lg shadow-lg p-8\">\n <h1 className=\"text-center mb-6\">Sign In</h1>\n\n {error && (\n <Alert className=\"mb-4 border-red-200 bg-red-50 text-red-700\">\n <p className=\"text-sm\">{error}</p>\n </Alert>\n )}\n\n {config?.features.local_auth && (\n <CredentialForm\n identifier={flow.identifier}\n password={flow.password}\n isLoading={isLoading}\n onIdentifierChange={flow.setIdentifier}\n onPasswordChange={flow.setPassword}\n onSubmit={flow.handleCredentialSubmit}\n />\n )}\n\n {config?.features.oauth && config.oauth?.providers && (\n <OAuthProviders providers={config.oauth.providers} />\n )}\n </div>\n </div>\n );\n}\n\n// -- Sub-components extracted to reduce Login complexity --\n\ninterface CredentialFormProps {\n identifier: string;\n password: string;\n isLoading: boolean;\n onIdentifierChange: (value: string) => void;\n onPasswordChange: (value: string) => void;\n onSubmit: (e: React.FormEvent) => void;\n}\n\nfunction CredentialForm({\n identifier,\n password,\n isLoading,\n onIdentifierChange,\n onPasswordChange,\n onSubmit,\n}: CredentialFormProps) {\n return (\n <form onSubmit={onSubmit} className=\"space-y-4\">\n <div className=\"space-y-2\">\n <Label htmlFor=\"identifier\">Email or Username</Label>\n <Input\n id=\"identifier\"\n type=\"text\"\n value={identifier}\n onChange={(e) => onIdentifierChange(e.target.value)}\n required\n placeholder=\"Enter your email or username\"\n />\n </div>\n\n <div className=\"space-y-2\">\n <Label htmlFor=\"password\">Password</Label>\n <Input\n id=\"password\"\n type=\"password\"\n value={password}\n onChange={(e) => onPasswordChange(e.target.value)}\n required\n placeholder=\"Enter your password\"\n />\n </div>\n\n <Button type=\"submit\" disabled={isLoading} className=\"w-full\">\n {isLoading ? \"Signing in...\" : \"Sign In\"}\n </Button>\n </form>\n );\n}\n\ninterface OAuthProvidersProps {\n providers: Record<\n string,\n { enabled: boolean; name: string; login_url: string }\n >;\n}\n\nfunction OAuthProviders({ providers }: OAuthProvidersProps) {\n return (\n <div className=\"mt-6\">\n <div className=\"relative mb-4\">\n <div className=\"absolute inset-0 flex items-center\">\n <div className=\"w-full border-t border-slate-300\"></div>\n </div>\n <div className=\"relative flex justify-center text-sm\">\n <span className=\"px-2 bg-white text-muted-foreground\">\n Or continue with\n </span>\n </div>\n </div>\n\n <div className=\"space-y-2\">\n {Object.entries(providers).map(([key, provider]) =>\n provider.enabled ? (\n <a\n key={key}\n href={provider.login_url}\n className=\"w-full flex items-center justify-center py-2 px-4 border border-slate-300 rounded-md hover:bg-slate-50\"\n >\n Continue with {provider.name}\n </a>\n ) : null,\n )}\n </div>\n </div>\n );\n}\n","import { useState, useCallback } from \"react\";\nimport { Check } from \"lucide-react\";\nimport { Button } from \"@petrarca/sonnet-ui\";\nimport { Alert } from \"@petrarca/sonnet-ui\";\n\n/**\n * TenantSelection -- shown after authentication when the user has\n * multiple tenants and no default is set. The user must pick a tenant\n * before the app loads.\n *\n * Fully props-driven with no app-specific imports.\n * Target package: @petrarca/sonnet-shell\n */\n\n/** Minimal tenant descriptor. Consumers map their domain type to this shape. */\nexport interface TenantOption {\n id: string;\n name: string;\n}\n\ninterface TenantSelectionProps {\n tenants: TenantOption[];\n isLoading: boolean;\n error: string | null;\n onSelect: (id: string) => void;\n}\n\nexport function TenantSelection({\n tenants,\n isLoading,\n error,\n onSelect,\n}: TenantSelectionProps): React.ReactElement {\n const [selectedId, setSelectedId] = useState<string | null>(null);\n\n const handleContinue = useCallback(() => {\n if (selectedId) {\n onSelect(selectedId);\n }\n }, [selectedId, onSelect]);\n\n return (\n <div className=\"flex items-center justify-center min-h-screen bg-slate-100\">\n <div className=\"w-full max-w-md bg-white rounded-lg shadow-lg p-8\">\n <h1 className=\"text-center mb-2\">Select Tenant</h1>\n <p className=\"text-center text-sm text-muted-foreground mb-6\">\n Choose the tenant you want to work in.\n </p>\n\n {error && (\n <Alert className=\"mb-4 border-red-200 bg-red-50 text-red-700\">\n <p className=\"text-sm\">{error}</p>\n </Alert>\n )}\n\n <div className=\"space-y-2 mb-6 max-h-64 overflow-y-auto\">\n {tenants.map((tenant) => (\n <button\n key={tenant.id}\n onClick={() => setSelectedId(tenant.id)}\n className={`w-full flex items-center justify-between px-4 py-3 rounded-md border text-left text-sm transition-colors ${\n selectedId === tenant.id\n ? \"border-blue-500 bg-blue-50 text-blue-900\"\n : \"border-slate-200 hover:bg-slate-50\"\n }`}\n >\n <span>{tenant.name}</span>\n {selectedId === tenant.id && (\n <Check className=\"h-4 w-4 text-blue-500\" />\n )}\n </button>\n ))}\n </div>\n\n <Button\n onClick={handleContinue}\n disabled={!selectedId || isLoading}\n className=\"w-full\"\n >\n {isLoading ? \"Loading...\" : \"Continue\"}\n </Button>\n </div>\n </div>\n );\n}\n","import { Navigate, useLocation } from \"react-router-dom\";\nimport { type ReactNode, useEffect, useRef, useState } from \"react\";\nimport { Spinner } from \"@petrarca/sonnet-ui\";\n\n/**\n * ProtectedRoute -- guards the app shell.\n *\n * On first mount, calls the injected `initializeAuth` callback so\n * the consumer can validate or restore a persisted session. While\n * initialization is in progress a loading spinner is shown.\n *\n * Once initialization is complete, guards based on:\n * - Auth disabled (`authEnabled` false): allow all access\n * - Not authenticated: redirect to /login with step=credentials\n * - Authenticated but no tenant: redirect to /login with step=tenant-selection\n *\n * All auth state is injected via props -- no direct store or context imports.\n * Target package: @petrarca/sonnet-shell\n */\n\nexport interface ProtectedRouteProps {\n children: ReactNode;\n /** Whether auth is globally enabled. When false, all access is allowed. */\n authEnabled: boolean;\n /** Whether the auth config is still loading. */\n authConfigLoading: boolean;\n /** Whether the user holds a valid session. */\n isAuthenticated: boolean;\n /** The current access token (null when not authenticated). */\n accessToken: string | null;\n /** The active tenant id (null when not yet selected). */\n activeTenantId: string | null;\n /** Called once on mount to restore and validate a persisted session. */\n initializeAuth: () => Promise<void>;\n}\n\nexport function ProtectedRoute({\n children,\n authEnabled,\n authConfigLoading,\n isAuthenticated,\n accessToken,\n activeTenantId,\n initializeAuth,\n}: ProtectedRouteProps): React.ReactElement {\n const location = useLocation();\n const [initializing, setInitializing] = useState(true);\n const initRef = useRef(initializeAuth);\n\n useEffect(() => {\n initRef.current().finally(() => setInitializing(false));\n }, []);\n\n if (authConfigLoading || initializing) {\n return (\n <div className=\"flex items-center justify-center min-h-screen\">\n <Spinner size=\"lg\" />\n </div>\n );\n }\n\n // Auth disabled: allow all access\n if (!authEnabled) {\n return <>{children}</>;\n }\n\n // Not authenticated: go to credentials step\n if (!isAuthenticated || !accessToken) {\n return (\n <Navigate\n to=\"/login\"\n state={{ from: location, step: \"credentials\" }}\n replace\n />\n );\n }\n\n // Authenticated but no tenant: go to tenant selection step\n if (!activeTenantId) {\n return (\n <Navigate\n to=\"/login\"\n state={{ from: location, step: \"tenant-selection\" }}\n replace\n />\n );\n }\n\n return <>{children}</>;\n}\n"],"mappings":"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","names":["useState","useCallback","Button","Alert","jsx","jsxs","useState","useCallback","Alert","Button","useLocation","useEffect","useState","jsx"]}
|
|
1
|
+
{"version":3,"sources":["../../src/auth/Login.tsx","../../src/auth/OrgSelection.tsx","../../src/auth/ProtectedRoute.tsx"],"sourcesContent":["import { useState, useEffect, useCallback } from \"react\";\nimport { useNavigate, useLocation } from \"react-router-dom\";\nimport { Input } from \"@petrarca/sonnet-ui\";\nimport { Label } from \"@petrarca/sonnet-ui\";\nimport { Spinner } from \"@petrarca/sonnet-ui\";\nimport { OrgSelection, type OrgOption } from \"./OrgSelection\";\nimport { normalizeError } from \"@petrarca/sonnet-core\";\n\n/**\n * Login -- multi-step authentication flow.\n *\n * Steps:\n * identifier -- email/username entry (when onDiscover is provided)\n * auth -- resolved providers: password form first, SSO buttons below\n * credentials -- legacy single-screen (no onDiscover)\n * org-selection -- organization picker after successful auth\n *\n * When onDiscover is NOT provided, falls back to the legacy single-screen\n * behaviour. Backward compatible.\n *\n * All async operations are injected via props. The component knows nothing\n * about OIDC, PKCE, Sestina, or discover endpoints.\n *\n * Design: centered card on gray-100, white card with elevated shadow,\n * consistent with Auth0 Universal Login / WorkOS AuthKit visual language.\n *\n * Target package: @petrarca/sonnet-shell\n */\n\nexport interface AuthConfig {\n enabled: boolean;\n features: { local_auth: boolean; oauth: boolean };\n oauth?: {\n providers: Record<\n string,\n { enabled: boolean; name: string; login_url: string }\n >;\n } | null;\n ui: { direct_redirect: string | null };\n}\n\nexport interface LoginProvider {\n type: \"local\" | \"external\";\n name: string;\n label: string;\n}\n\ntype LoginStep = \"identifier\" | \"auth\" | \"credentials\" | \"org-selection\";\n\ninterface LocationState {\n from?: { pathname: string };\n step?: LoginStep;\n}\n\nexport interface LoginProps {\n config: AuthConfig | null;\n isAuthenticated: boolean;\n activeTenantId: string | null;\n isLoading: boolean;\n error: string | null;\n orgs: OrgOption[] | null;\n onLogin: (credentials: {\n identifier: string;\n password: string;\n }) => Promise<void>;\n onFetchCurrentUser: () => Promise<void>;\n onSelectOrg: (id: string) => Promise<void>;\n onClearError: () => void;\n /** Application name shown above the card as a brand label. */\n appName?: string;\n /** Logo URL shown above the card. If provided, appName is used as alt text. */\n logoUrl?: string;\n /** Enables identifier-first flow. App calls discover endpoint and returns providers. */\n onDiscover?: (identifier: string) => Promise<LoginProvider[]>;\n /** Called when user clicks an external provider button. App handles PKCE + redirect. */\n onExternalLogin?: (provider: LoginProvider) => Promise<void> | void;\n /** Whether discovery is in flight. */\n isDiscovering?: boolean;\n}\n\n// -- Flow state hook --\n\nfunction useLoginFlow(props: LoginProps) {\n const {\n config,\n isAuthenticated,\n activeTenantId,\n orgs,\n onLogin,\n onFetchCurrentUser,\n onSelectOrg,\n onClearError,\n onDiscover,\n onExternalLogin,\n } = props;\n\n const navigate = useNavigate();\n const location = useLocation();\n const locationState = location.state as LocationState | null;\n const identifierFirst = Boolean(onDiscover);\n const initial: LoginStep =\n locationState?.step ?? (identifierFirst ? \"identifier\" : \"credentials\");\n\n const [redirectTo] = useState(() => locationState?.from?.pathname ?? \"/\");\n const [step, setStep] = useState<LoginStep>(initial);\n const [identifier, setIdentifier] = useState(\"\");\n const [password, setPassword] = useState(\"\");\n const [providers, setProviders] = useState<LoginProvider[]>([]);\n const [discoverError, setDiscoverError] = useState<string | null>(null);\n const [isRedirecting, setIsRedirecting] = useState(false);\n const [orgError, setOrgError] = useState<string | null>(null);\n\n useEffect(() => {\n if (isAuthenticated && activeTenantId)\n navigate(redirectTo, { replace: true });\n }, [isAuthenticated, activeTenantId, navigate, redirectTo]);\n\n useEffect(() => {\n if (!isAuthenticated || !orgs || activeTenantId) return;\n if (orgs.length === 1) {\n onSelectOrg(orgs[0].id).catch((err) => {\n setOrgError(\n normalizeError(err).message || \"Failed to select organization\",\n );\n setStep(\"org-selection\");\n });\n } else if (orgs.length > 1) {\n setStep(\"org-selection\");\n }\n }, [isAuthenticated, orgs, activeTenantId, onSelectOrg]);\n\n useEffect(() => {\n if (config?.ui.direct_redirect)\n window.location.href = config.ui.direct_redirect;\n }, [config]);\n\n const handleIdentifierSubmit = useCallback(\n async (e: React.FormEvent) => {\n e.preventDefault();\n if (!onDiscover || !identifier.trim()) return;\n setDiscoverError(null);\n try {\n const resolved = await onDiscover(identifier.trim());\n setProviders(resolved);\n setStep(\"auth\");\n } catch (err) {\n setDiscoverError(\n normalizeError(err).message || \"Failed to look up providers\",\n );\n }\n },\n [identifier, onDiscover],\n );\n\n const handleCredentialSubmit = useCallback(\n async (e: React.FormEvent) => {\n e.preventDefault();\n onClearError();\n try {\n await onLogin({ identifier, password });\n await onFetchCurrentUser();\n } catch {\n /* error via props */\n }\n },\n [identifier, password, onLogin, onFetchCurrentUser, onClearError],\n );\n\n const handleExternalLogin = useCallback(\n async (provider: LoginProvider) => {\n if (!onExternalLogin) return;\n setIsRedirecting(true);\n try {\n await onExternalLogin(provider);\n } catch {\n setIsRedirecting(false);\n }\n },\n [onExternalLogin],\n );\n\n const handleOrgSelect = useCallback(\n async (id: string) => {\n setOrgError(null);\n try {\n await onSelectOrg(id);\n } catch (err) {\n setOrgError(\n normalizeError(err).message || \"Failed to select organization\",\n );\n }\n },\n [onSelectOrg],\n );\n\n const goBack = useCallback(() => {\n setPassword(\"\");\n setDiscoverError(null);\n onClearError();\n setStep(identifierFirst ? \"identifier\" : \"credentials\");\n }, [identifierFirst, onClearError]);\n\n return {\n step,\n identifier,\n setIdentifier,\n password,\n setPassword,\n providers,\n discoverError,\n isRedirecting,\n orgError,\n handleIdentifierSubmit,\n handleCredentialSubmit,\n handleExternalLogin,\n handleOrgSelect,\n goBack,\n };\n}\n\n// -- Main component --\n\nexport function Login(props: LoginProps): React.ReactElement | null {\n const { config, isLoading, isDiscovering, error, orgs, appName, logoUrl } =\n props;\n const flow = useLoginFlow(props);\n\n if (config?.ui.direct_redirect || flow.isRedirecting)\n return <RedirectingMessage />;\n\n if (flow.step === \"org-selection\" && orgs) {\n return (\n <OrgSelection\n orgs={orgs}\n isLoading={isLoading}\n error={flow.orgError}\n onSelect={flow.handleOrgSelect}\n />\n );\n }\n\n return (\n <PageShell>\n <Card>\n {flow.step === \"identifier\" && (\n <IdentifierStep\n appName={appName}\n logoUrl={logoUrl}\n identifier={flow.identifier}\n error={flow.discoverError}\n isLoading={isDiscovering ?? false}\n onIdentifierChange={flow.setIdentifier}\n onSubmit={flow.handleIdentifierSubmit}\n />\n )}\n\n {flow.step === \"auth\" && (\n <AuthStep\n appName={appName}\n logoUrl={logoUrl}\n identifier={flow.identifier}\n providers={flow.providers}\n password={flow.password}\n error={error}\n isLoading={isLoading}\n onPasswordChange={flow.setPassword}\n onCredentialSubmit={flow.handleCredentialSubmit}\n onExternalLogin={flow.handleExternalLogin}\n onBack={flow.goBack}\n />\n )}\n\n {flow.step === \"credentials\" && (\n <CredentialsStep\n appName={appName}\n logoUrl={logoUrl}\n config={config}\n identifier={flow.identifier}\n password={flow.password}\n error={error}\n isLoading={isLoading}\n onIdentifierChange={flow.setIdentifier}\n onPasswordChange={flow.setPassword}\n onSubmit={flow.handleCredentialSubmit}\n onExternalLogin={flow.handleExternalLogin}\n />\n )}\n </Card>\n </PageShell>\n );\n}\n\n// -- Layout --\n\nfunction PageShell({ children }: { children: React.ReactNode }) {\n return (\n <div className=\"flex flex-col items-center justify-center min-h-screen bg-gray-100 px-4\">\n {children}\n </div>\n );\n}\n\nfunction Card({ children }: { children: React.ReactNode }) {\n return (\n <div\n className=\"w-full max-w-sm bg-white rounded-xl border border-gray-200 p-8\"\n style={{\n boxShadow:\n \"0 24px 48px rgba(15,23,42,0.10), 0 2px 4px rgba(15,23,42,0.06)\",\n }}\n >\n {children}\n </div>\n );\n}\n\n// -- Shared brand header (inside the card) --\n\nfunction BrandHeader({\n appName,\n logoUrl,\n}: {\n appName?: string;\n logoUrl?: string;\n}) {\n if (!appName && !logoUrl) return null;\n return (\n <div className=\"flex flex-col items-center mb-6\">\n {logoUrl ? (\n <img src={logoUrl} alt={appName ?? \"Logo\"} className=\"h-10 w-auto\" />\n ) : (\n <span className=\"text-lg font-semibold text-slate-500 tracking-wide\">\n {appName}\n </span>\n )}\n </div>\n );\n}\n\n// -- Identifier step --\n\ninterface IdentifierStepProps {\n appName?: string;\n logoUrl?: string;\n identifier: string;\n error: string | null;\n isLoading: boolean;\n onIdentifierChange: (value: string) => void;\n onSubmit: (e: React.FormEvent) => void;\n}\n\nfunction IdentifierStep({\n appName,\n logoUrl,\n identifier,\n error,\n isLoading,\n onIdentifierChange,\n onSubmit,\n}: IdentifierStepProps) {\n return (\n <form onSubmit={onSubmit} className=\"space-y-6\">\n <BrandHeader appName={appName} logoUrl={logoUrl} />\n <div className=\"text-center\">\n <h1 className=\"text-2xl font-semibold text-slate-900 leading-tight\">\n Welcome\n </h1>\n <p className=\"mt-2 text-sm text-gray-500\">\n Enter your email or username to continue.\n </p>\n </div>\n\n {error ? <ErrorAlert message={error} /> : null}\n\n <div>\n <Label\n htmlFor=\"identifier\"\n className=\"block text-sm font-medium text-gray-700 mb-1.5\"\n >\n Email or username\n </Label>\n <Input\n id=\"identifier\"\n type=\"text\"\n value={identifier}\n onChange={(e) => onIdentifierChange(e.target.value)}\n required\n autoFocus\n autoComplete=\"email\"\n placeholder=\"you@company.com\"\n className=\"h-10 rounded-lg border-gray-300 text-sm placeholder:text-gray-400 focus:ring-indigo-500 focus:border-indigo-500\"\n />\n </div>\n\n <PrimaryButton\n type=\"submit\"\n disabled={isLoading || !identifier.trim()}\n loading={isLoading}\n loadingText=\"Looking up...\"\n >\n Continue\n </PrimaryButton>\n </form>\n );\n}\n\n// -- Auth step (identifier-first resolved providers) --\n\ninterface AuthStepProps {\n appName?: string;\n logoUrl?: string;\n identifier: string;\n providers: LoginProvider[];\n password: string;\n error: string | null;\n isLoading: boolean;\n onPasswordChange: (value: string) => void;\n onCredentialSubmit: (e: React.FormEvent) => void;\n onExternalLogin: (provider: LoginProvider) => void;\n onBack: () => void;\n}\n\nfunction AuthStep({\n appName,\n logoUrl,\n identifier,\n providers,\n password,\n error,\n isLoading,\n onPasswordChange,\n onCredentialSubmit,\n onExternalLogin,\n onBack,\n}: AuthStepProps) {\n const external = providers.filter((p) => p.type === \"external\");\n const hasLocal = providers.some((p) => p.type === \"local\");\n const hasBoth = external.length > 0 && hasLocal;\n\n return (\n <div className=\"space-y-6\">\n <BrandHeader appName={appName} logoUrl={logoUrl} />\n <div>\n <button\n type=\"button\"\n onClick={onBack}\n className=\"flex items-center gap-1.5 text-sm text-gray-500 hover:text-gray-700 transition-colors mb-4\"\n >\n <span aria-hidden>←</span> Back\n </button>\n <h1 className=\"text-2xl font-semibold text-slate-900 leading-tight\">\n Welcome back\n </h1>\n <p className=\"mt-1 text-sm text-gray-500\">{identifier}</p>\n </div>\n\n {error ? <ErrorAlert message={error} /> : null}\n\n {hasLocal ? (\n <form onSubmit={onCredentialSubmit} className=\"space-y-4\">\n <div>\n <Label\n htmlFor=\"password\"\n className=\"block text-sm font-medium text-gray-700 mb-1.5\"\n >\n Password\n </Label>\n <Input\n id=\"password\"\n type=\"password\"\n value={password}\n onChange={(e) => onPasswordChange(e.target.value)}\n required\n autoFocus\n autoComplete=\"current-password\"\n placeholder=\"Enter your password\"\n className=\"h-10 rounded-lg border-gray-300 text-sm placeholder:text-gray-400 focus:ring-indigo-500 focus:border-indigo-500\"\n />\n </div>\n <PrimaryButton\n type=\"submit\"\n disabled={isLoading}\n loading={isLoading}\n loadingText=\"Signing in...\"\n >\n Sign in\n </PrimaryButton>\n </form>\n ) : null}\n\n {hasBoth ? <Separator /> : null}\n\n {external.length > 0 ? (\n <div className=\"space-y-2\">\n {external.map((p) => (\n <SsoButton\n key={p.name}\n label={p.label}\n onClick={() => onExternalLogin(p)}\n />\n ))}\n </div>\n ) : null}\n\n {providers.length === 0 ? (\n <p className=\"text-sm text-gray-500 text-center py-2\">\n No login methods available.\n </p>\n ) : null}\n </div>\n );\n}\n\n// -- Legacy credentials step --\n\ninterface CredentialsStepProps {\n appName?: string;\n logoUrl?: string;\n config: AuthConfig | null;\n identifier: string;\n password: string;\n error: string | null;\n isLoading: boolean;\n onIdentifierChange: (value: string) => void;\n onPasswordChange: (value: string) => void;\n onSubmit: (e: React.FormEvent) => void;\n onExternalLogin: (provider: LoginProvider) => void;\n}\n\nfunction resolveOAuthProviders(config: AuthConfig | null) {\n return config?.features.oauth && config.oauth?.providers\n ? Object.entries(config.oauth.providers).filter(([, p]) => p.enabled)\n : [];\n}\n\nfunction CredentialsStep({\n appName,\n logoUrl,\n config,\n identifier,\n password,\n error,\n isLoading,\n onIdentifierChange,\n onPasswordChange,\n onSubmit,\n onExternalLogin,\n}: CredentialsStepProps) {\n const oauthProviders = resolveOAuthProviders(config);\n const hasBoth = config?.features.local_auth && oauthProviders.length > 0;\n\n return (\n <div className=\"space-y-6\">\n <BrandHeader appName={appName} logoUrl={logoUrl} />\n <h1 className=\"text-2xl font-semibold text-slate-900 leading-tight text-center\">\n Welcome\n </h1>\n\n {error ? <ErrorAlert message={error} /> : null}\n\n {config?.features.local_auth ? (\n <form onSubmit={onSubmit} className=\"space-y-4\">\n <div>\n <Label\n htmlFor=\"identifier\"\n className=\"block text-sm font-medium text-gray-700 mb-1.5\"\n >\n Email or username\n </Label>\n <Input\n id=\"identifier\"\n type=\"text\"\n value={identifier}\n onChange={(e) => onIdentifierChange(e.target.value)}\n required\n autoFocus\n autoComplete=\"email\"\n placeholder=\"you@company.com\"\n className=\"h-10 rounded-lg border-gray-300 text-sm placeholder:text-gray-400 focus:ring-indigo-500 focus:border-indigo-500\"\n />\n </div>\n <div>\n <Label\n htmlFor=\"password\"\n className=\"block text-sm font-medium text-gray-700 mb-1.5\"\n >\n Password\n </Label>\n <Input\n id=\"password\"\n type=\"password\"\n value={password}\n onChange={(e) => onPasswordChange(e.target.value)}\n required\n autoComplete=\"current-password\"\n placeholder=\"Enter your password\"\n className=\"h-10 rounded-lg border-gray-300 text-sm placeholder:text-gray-400 focus:ring-indigo-500 focus:border-indigo-500\"\n />\n </div>\n <PrimaryButton\n type=\"submit\"\n disabled={isLoading}\n loading={isLoading}\n loadingText=\"Signing in...\"\n >\n Sign in\n </PrimaryButton>\n </form>\n ) : null}\n\n {hasBoth ? <Separator /> : null}\n\n {oauthProviders.length > 0 ? (\n <div className=\"space-y-2\">\n {oauthProviders.map(([key, p]) => (\n <SsoButton\n key={key}\n label={`Continue with ${p.name}`}\n onClick={() =>\n onExternalLogin({ type: \"external\", name: key, label: p.name })\n }\n />\n ))}\n </div>\n ) : null}\n </div>\n );\n}\n\n// -- Shared primitives --\n\nfunction RedirectingMessage() {\n return (\n <div className=\"flex flex-col items-center justify-center min-h-screen bg-gray-100 gap-3 text-gray-500\">\n <Spinner size=\"lg\" />\n <p className=\"text-sm\">Redirecting...</p>\n </div>\n );\n}\n\nfunction ErrorAlert({ message }: { message: string }) {\n return (\n <div className=\"rounded-lg border border-red-200 bg-red-50 px-3 py-2.5 text-sm text-red-700\">\n {message}\n </div>\n );\n}\n\nfunction Separator() {\n return (\n <div className=\"relative\">\n <div className=\"absolute inset-0 flex items-center\">\n <div className=\"w-full border-t border-gray-200\" />\n </div>\n <div className=\"relative flex justify-center text-xs\">\n <span className=\"bg-white px-2 text-gray-400\">or continue with</span>\n </div>\n </div>\n );\n}\n\ninterface PrimaryButtonProps {\n type?: \"submit\" | \"button\";\n disabled?: boolean;\n loading?: boolean;\n loadingText?: string;\n children: React.ReactNode;\n onClick?: () => void;\n}\n\nfunction PrimaryButton({\n type = \"button\",\n disabled,\n loading,\n loadingText,\n children,\n onClick,\n}: PrimaryButtonProps) {\n return (\n <button\n type={type}\n disabled={disabled}\n onClick={onClick}\n className=\"w-full inline-flex items-center justify-center rounded-full bg-indigo-600 px-5 py-2.5 text-sm font-semibold text-white shadow-md transition duration-150 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed\"\n style={{\n boxShadow: disabled ? undefined : \"0 4px 14px rgba(79,70,229,0.30)\",\n }}\n >\n {loading ? <Spinner size=\"sm\" className=\"mr-2\" /> : null}\n {loading && loadingText ? loadingText : children}\n </button>\n );\n}\n\nfunction SsoButton({ label, onClick }: { label: string; onClick: () => void }) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n className=\"w-full flex items-center justify-center gap-2.5 h-10 px-4 rounded-lg border border-gray-300 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50 hover:border-gray-400 transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2\"\n >\n {label}\n </button>\n );\n}\n","import { useState, useCallback } from \"react\";\nimport { Check } from \"lucide-react\";\nimport { Button } from \"@petrarca/sonnet-ui\";\nimport { Alert } from \"@petrarca/sonnet-ui\";\n\n/**\n * OrgSelection -- shown after authentication when the user belongs to\n * multiple organizations and no default is set. The user selects which\n * organization context to act in before the app loads.\n *\n * \"Organization\" is used generically: in Sestina this is an org-unit; in\n * another app it could be a tenant, workspace, team, or company. The\n * consumer maps their domain type to OrgOption.\n *\n * Fully props-driven with no app-specific imports.\n * Target package: @petrarca/sonnet-shell\n */\n\n/** Minimal organization descriptor. Consumers map their domain type to this shape. */\nexport interface OrgOption {\n id: string;\n name: string;\n}\n\ninterface OrgSelectionProps {\n orgs: OrgOption[];\n isLoading: boolean;\n error: string | null;\n onSelect: (id: string) => void;\n}\n\nexport function OrgSelection({\n orgs,\n isLoading,\n error,\n onSelect,\n}: OrgSelectionProps): React.ReactElement {\n const [selectedId, setSelectedId] = useState<string | null>(null);\n\n const handleContinue = useCallback(() => {\n if (selectedId) onSelect(selectedId);\n }, [selectedId, onSelect]);\n\n return (\n <div className=\"flex items-center justify-center min-h-screen bg-slate-50 px-4\">\n <div className=\"w-full max-w-sm bg-white rounded-xl shadow-sm border border-slate-200 p-8\">\n <div className=\"mb-6\">\n <h1 className=\"text-2xl font-semibold text-slate-900 text-center\">\n Select organization\n </h1>\n <p className=\"text-center text-sm text-slate-500 mt-1\">\n Choose the organization you want to work in.\n </p>\n </div>\n\n {error ? (\n <Alert className=\"mb-4 border-red-200 bg-red-50 text-red-700 text-sm py-2.5 px-3\">\n {error}\n </Alert>\n ) : null}\n\n <div className=\"space-y-2 mb-6 max-h-64 overflow-y-auto\">\n {orgs.map((org) => (\n <button\n key={org.id}\n onClick={() => setSelectedId(org.id)}\n className={`w-full flex items-center justify-between px-4 py-3 rounded-lg border text-left text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-slate-400 ${\n selectedId === org.id\n ? \"border-blue-500 bg-blue-50 text-blue-900\"\n : \"border-slate-200 hover:bg-slate-50 text-slate-700\"\n }`}\n >\n <span className=\"font-medium\">{org.name}</span>\n {selectedId === org.id ? (\n <Check className=\"h-4 w-4 text-blue-500 shrink-0\" />\n ) : null}\n </button>\n ))}\n </div>\n\n <Button\n onClick={handleContinue}\n disabled={!selectedId || isLoading}\n className=\"w-full h-10\"\n >\n {isLoading ? \"Loading...\" : \"Continue\"}\n </Button>\n </div>\n </div>\n );\n}\n","import { Navigate, useLocation } from \"react-router-dom\";\nimport { type ReactNode, useEffect, useRef, useState } from \"react\";\nimport { Spinner } from \"@petrarca/sonnet-ui\";\n\n/**\n * ProtectedRoute -- guards the app shell.\n *\n * On first mount, calls the injected `initializeAuth` callback so\n * the consumer can validate or restore a persisted session. While\n * initialization is in progress a loading spinner is shown.\n *\n * Once initialization is complete, guards based on:\n * - Auth disabled (`authEnabled` false): allow all access\n * - Not authenticated: redirect to /login with step=credentials\n * - Authenticated but no tenant: redirect to /login with step=tenant-selection\n *\n * All auth state is injected via props -- no direct store or context imports.\n * Target package: @petrarca/sonnet-shell\n */\n\nexport interface ProtectedRouteProps {\n children: ReactNode;\n /** Whether auth is globally enabled. When false, all access is allowed. */\n authEnabled: boolean;\n /** Whether the auth config is still loading. */\n authConfigLoading: boolean;\n /** Whether the user holds a valid session. */\n isAuthenticated: boolean;\n /** The current access token (null when not authenticated). */\n accessToken: string | null;\n /** The active tenant id (null when not yet selected). */\n activeTenantId: string | null;\n /** Called once on mount to restore and validate a persisted session. */\n initializeAuth: () => Promise<void>;\n}\n\nexport function ProtectedRoute({\n children,\n authEnabled,\n authConfigLoading,\n isAuthenticated,\n accessToken,\n activeTenantId,\n initializeAuth,\n}: ProtectedRouteProps): React.ReactElement {\n const location = useLocation();\n const [initializing, setInitializing] = useState(true);\n const initRef = useRef(initializeAuth);\n\n useEffect(() => {\n initRef.current().finally(() => setInitializing(false));\n }, []);\n\n if (authConfigLoading || initializing) {\n return (\n <div className=\"flex items-center justify-center min-h-screen\">\n <Spinner size=\"lg\" />\n </div>\n );\n }\n\n // Auth disabled: allow all access\n if (!authEnabled) {\n return <>{children}</>;\n }\n\n // Not authenticated: go to credentials step\n if (!isAuthenticated || !accessToken) {\n return (\n <Navigate\n to=\"/login\"\n state={{ from: location, step: \"credentials\" }}\n replace\n />\n );\n }\n\n // Authenticated but no tenant: go to tenant selection step\n if (!activeTenantId) {\n return (\n <Navigate\n to=\"/login\"\n state={{ from: location, step: \"tenant-selection\" }}\n replace\n />\n );\n }\n\n return <>{children}</>;\n}\n"],"mappings":"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","names":["useState","useCallback","jsx","jsxs","useState","useCallback","useLocation","useEffect","useState","Spinner","jsx"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -138,6 +138,20 @@ interface ShellModule {
|
|
|
138
138
|
icon: LucideIcon;
|
|
139
139
|
/** URL prefix used to match this module to the current route */
|
|
140
140
|
basePath: string;
|
|
141
|
+
/**
|
|
142
|
+
* Additional URL prefixes (besides `basePath`) that resolve to this module.
|
|
143
|
+
*
|
|
144
|
+
* Use when a single icon-rail entry owns several route trees that do not
|
|
145
|
+
* share a common prefix -- e.g. an "Organization" module whose grouped
|
|
146
|
+
* sub-nav links to `/accounts`, `/tenants`, and `/org-units`. The rail
|
|
147
|
+
* highlight and sub-nav panel then activate on any of these prefixes, while
|
|
148
|
+
* the underlying routes keep their own paths.
|
|
149
|
+
*
|
|
150
|
+
* Matching uses `pathname.startsWith(prefix)`, identical to `basePath`.
|
|
151
|
+
* `basePath` remains the module's canonical/first prefix (used for
|
|
152
|
+
* navigation targets); `matchPaths` only extends route-to-module resolution.
|
|
153
|
+
*/
|
|
154
|
+
matchPaths?: string[];
|
|
141
155
|
/**
|
|
142
156
|
* Fixed top-level navigation links shown in the sidebar's top zone
|
|
143
157
|
* (no group heading, separator below). Not shown in the icon-rail
|
package/dist/index.js
CHANGED
|
@@ -1089,20 +1089,26 @@ function useShellApiInit(deps) {
|
|
|
1089
1089
|
}
|
|
1090
1090
|
function useActiveService(modules) {
|
|
1091
1091
|
const location = useLocation2();
|
|
1092
|
-
const
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1092
|
+
const prefixToService = useMemo5(() => {
|
|
1093
|
+
const pairs = [];
|
|
1094
|
+
for (const m of modules) {
|
|
1095
|
+
if (m.hidden) continue;
|
|
1096
|
+
const prefixes = [m.basePath, ...m.matchPaths ?? []];
|
|
1097
|
+
for (const prefix of prefixes) {
|
|
1098
|
+
if (prefix !== "/") pairs.push([prefix, m.id]);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
pairs.sort((a, b) => b[0].length - a[0].length);
|
|
1102
|
+
return pairs;
|
|
1103
|
+
}, [modules]);
|
|
1098
1104
|
const resolveServiceFromPath = useCallback2(
|
|
1099
1105
|
(pathname) => {
|
|
1100
|
-
for (const [
|
|
1101
|
-
if (pathname.startsWith(
|
|
1106
|
+
for (const [prefix, id] of prefixToService) {
|
|
1107
|
+
if (pathname.startsWith(prefix)) return id;
|
|
1102
1108
|
}
|
|
1103
1109
|
return null;
|
|
1104
1110
|
},
|
|
1105
|
-
[
|
|
1111
|
+
[prefixToService]
|
|
1106
1112
|
);
|
|
1107
1113
|
const [selectedServiceId, setSelectedServiceId] = useState2(
|
|
1108
1114
|
() => resolveServiceFromPath(location.pathname)
|