security: Add proper authentication, RBAC, and tenant isolation
- Add password hashing with bcrypt - Add AuthService with proper login - Add password strength validation - Add RBAC middleware (AdminOnly, ManagerOrAdmin) - Add tenant isolation middleware - Update CRM handler with tenant filtering - Add JWT fallback for development mode - Add user context helpers - Build successful
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
import { useState, useCallback, useRef } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { RefreshCw } from 'lucide-react'
|
||||
|
||||
interface PullToRefreshProps {
|
||||
onRefresh: () => Promise<void>
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function PullToRefresh({ onRefresh, children, className }: PullToRefreshProps) {
|
||||
const [pulling, setPulling] = useState(false)
|
||||
const [pullDistance, setPullDistance] = useState(0)
|
||||
const [refreshing, setRefreshing] = useState(false)
|
||||
const touchStartY = useRef(0)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const maxPullDistance = 100
|
||||
const refreshThreshold = 80
|
||||
|
||||
const onTouchStart = useCallback((e: React.TouchEvent) => {
|
||||
// Only allow pull-to-refresh when at top of scroll
|
||||
if (containerRef.current && containerRef.current.scrollTop === 0) {
|
||||
touchStartY.current = e.targetTouches[0].clientY
|
||||
setPulling(true)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const onTouchMove = useCallback((e: React.TouchEvent) => {
|
||||
if (!pulling) return
|
||||
|
||||
const currentY = e.targetTouches[0].clientY
|
||||
const diff = currentY - touchStartY.current
|
||||
|
||||
if (diff > 0) {
|
||||
// Resistance increases as user pulls further
|
||||
const resistance = 1 + (diff / maxPullDistance) * 0.5
|
||||
const newDistance = Math.min(diff / resistance, maxPullDistance)
|
||||
setPullDistance(newDistance)
|
||||
}
|
||||
}, [pulling])
|
||||
|
||||
const onTouchEnd = useCallback(async () => {
|
||||
if (!pulling) return
|
||||
|
||||
if (pullDistance >= refreshThreshold && !refreshing) {
|
||||
setRefreshing(true)
|
||||
try {
|
||||
await onRefresh()
|
||||
} finally {
|
||||
setRefreshing(false)
|
||||
}
|
||||
}
|
||||
|
||||
setPulling(false)
|
||||
setPullDistance(0)
|
||||
}, [pulling, pullDistance, refreshing, onRefresh])
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={cn('relative overflow-y-auto', className)}
|
||||
onTouchStart={onTouchStart}
|
||||
onTouchMove={onTouchMove}
|
||||
onTouchEnd={onTouchEnd}
|
||||
>
|
||||
{/* Pull indicator */}
|
||||
<div
|
||||
className="absolute top-0 left-0 right-0 flex items-center justify-center transition-transform"
|
||||
style={{
|
||||
transform: `translateY(${pullDistance - 60}px)`,
|
||||
opacity: pulling ? 1 : 0,
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<RefreshCw
|
||||
size={24}
|
||||
className={cn(
|
||||
'text-primary transition-transform',
|
||||
refreshing && 'animate-spin',
|
||||
!refreshing && pullDistance >= refreshThreshold && 'rotate-180'
|
||||
)}
|
||||
/>
|
||||
<span className="text-xs text-text-secondary">
|
||||
{refreshing ? 'Refreshing...' : pullDistance >= refreshThreshold ? 'Release to refresh' : 'Pull to refresh'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content with offset when pulling */}
|
||||
<div
|
||||
style={{
|
||||
transform: pulling ? `translateY(${pullDistance}px)` : 'translateY(0)',
|
||||
transition: pulling ? 'none' : 'transform 0.3s ease-out',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user