Skip to content

Commit 4b03335

Browse files
committed
feat: add @hanzo/ui/auth package with IAMLoginButton and AuthGuard
New auth components for unified IAM integration: - IAMLoginButton: "Sign in with Hanzo" button that initiates OAuth flow - AuthGuard: wrapper that redirects to IAM when unauthenticated - Re-exports useHanzoAuth, UserOrgDropdown, HanzoUser/HanzoOrg types Adds ./auth and ./auth/* subpath exports to package.json.
1 parent bf4ef83 commit 4b03335

4 files changed

Lines changed: 147 additions & 0 deletions

File tree

pkg/ui/package.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,16 @@
514514
"import": "./dist/pattern/*.mjs",
515515
"require": "./dist/pattern/*.js"
516516
},
517+
"./auth": {
518+
"types": "./dist/auth/index.d.ts",
519+
"import": "./dist/auth/index.mjs",
520+
"require": "./dist/auth/index.js"
521+
},
522+
"./auth/*": {
523+
"types": "./dist/auth/*.d.ts",
524+
"import": "./dist/auth/*.mjs",
525+
"require": "./dist/auth/*.js"
526+
},
517527
"./navigation": {
518528
"types": "./dist/navigation/index.d.ts",
519529
"import": "./dist/navigation/index.mjs",

pkg/ui/src/auth/AuthGuard.tsx

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
'use client'
2+
3+
import React, { useEffect } from 'react'
4+
import { useHanzoAuth } from '../navigation/hanzo-shell/useHanzoAuth'
5+
6+
export interface AuthGuardProps {
7+
/** Where to redirect when not authenticated (defaults to IAM login) */
8+
loginUrl?: string
9+
/** OAuth client ID for IAM redirect */
10+
clientId?: string
11+
/** Show loading state while checking auth */
12+
fallback?: React.ReactNode
13+
children: React.ReactNode
14+
}
15+
16+
/**
17+
* Wrapper component that redirects to IAM login if user is not authenticated.
18+
* Uses useHanzoAuth to check localStorage token.
19+
*/
20+
export function AuthGuard({
21+
loginUrl,
22+
clientId,
23+
fallback,
24+
children,
25+
}: AuthGuardProps) {
26+
const { user, loading } = useHanzoAuth()
27+
28+
useEffect(() => {
29+
if (!loading && !user) {
30+
if (loginUrl) {
31+
window.location.href = loginUrl
32+
} else if (clientId) {
33+
const redirect = `${window.location.origin}/auth/callback`
34+
const url = new URL('https://hanzo.id/login/oauth/authorize')
35+
url.searchParams.set('client_id', clientId)
36+
url.searchParams.set('response_type', 'code')
37+
url.searchParams.set('redirect_uri', redirect)
38+
url.searchParams.set('scope', 'openid profile email')
39+
window.location.href = url.toString()
40+
} else {
41+
window.location.href = 'https://hanzo.id'
42+
}
43+
}
44+
}, [loading, user, loginUrl, clientId])
45+
46+
if (loading) {
47+
return (
48+
<>
49+
{fallback ?? (
50+
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', background: '#09090b', color: '#888' }}>
51+
Loading...
52+
</div>
53+
)}
54+
</>
55+
)
56+
}
57+
58+
if (!user) return null
59+
60+
return <>{children}</>
61+
}

pkg/ui/src/auth/IAMLoginButton.tsx

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
'use client'
2+
3+
import React, { useCallback, useState } from 'react'
4+
5+
export interface IAMLoginButtonProps {
6+
/** IAM server URL (e.g., https://hanzo.id) */
7+
iamUrl?: string
8+
/** OAuth client ID */
9+
clientId: string
10+
/** OAuth redirect URI (defaults to current origin + /auth/callback) */
11+
redirectUri?: string
12+
/** Button label */
13+
label?: string
14+
/** Additional CSS classes */
15+
className?: string
16+
/** Button variant: 'default' (filled) or 'outline' */
17+
variant?: 'default' | 'outline'
18+
}
19+
20+
/**
21+
* "Sign in with Hanzo" button that initiates IAM OAuth flow.
22+
* Zero-dependency — works in any React app.
23+
*/
24+
export function IAMLoginButton({
25+
iamUrl = 'https://hanzo.id',
26+
clientId,
27+
redirectUri,
28+
label = 'Sign in with Hanzo',
29+
className = '',
30+
variant = 'default',
31+
}: IAMLoginButtonProps) {
32+
const [loading, setLoading] = useState(false)
33+
34+
const handleClick = useCallback(() => {
35+
setLoading(true)
36+
const redirect = redirectUri || `${window.location.origin}/auth/callback`
37+
const state = crypto.randomUUID()
38+
39+
const url = new URL(`${iamUrl}/login/oauth/authorize`)
40+
url.searchParams.set('client_id', clientId)
41+
url.searchParams.set('response_type', 'code')
42+
url.searchParams.set('redirect_uri', redirect)
43+
url.searchParams.set('scope', 'openid profile email')
44+
url.searchParams.set('state', state)
45+
46+
window.location.href = url.toString()
47+
}, [iamUrl, clientId, redirectUri])
48+
49+
const baseStyles = 'inline-flex items-center justify-center gap-2 rounded-md px-4 py-2.5 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50'
50+
const variantStyles = variant === 'outline'
51+
? 'border border-[#333] bg-transparent text-white hover:bg-[#1a1a1f]'
52+
: 'bg-white text-black hover:bg-[#e5e5e5]'
53+
54+
return (
55+
<button
56+
type="button"
57+
className={`${baseStyles} ${variantStyles} ${className}`}
58+
onClick={handleClick}
59+
disabled={loading}
60+
>
61+
<svg width="16" height="16" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
62+
<text x="4" y="26" fontFamily="-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif" fontSize="28" fontWeight="700" fill="currentColor">H</text>
63+
</svg>
64+
{loading ? 'Redirecting...' : label}
65+
</button>
66+
)
67+
}

pkg/ui/src/auth/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export { IAMLoginButton } from './IAMLoginButton'
2+
export type { IAMLoginButtonProps } from './IAMLoginButton'
3+
export { AuthGuard } from './AuthGuard'
4+
export type { AuthGuardProps } from './AuthGuard'
5+
6+
// Re-export auth-related navigation components for convenience
7+
export { useHanzoAuth } from '../navigation/hanzo-shell/useHanzoAuth'
8+
export { UserOrgDropdown } from '../navigation/hanzo-shell/UserOrgDropdown'
9+
export type { HanzoUser, HanzoOrg } from '../navigation/hanzo-shell/types'

0 commit comments

Comments
 (0)