feat: HR Portal - Complete Multi-Tenant System with Redis Session Storage
Major Features: - ✅ Multi-tenant architecture (tenant isolation) - ✅ Employee CRUD with lifecycle management (onboarding/offboarding) - ✅ Department tree structure with email domain management - ✅ Company info management (single-record editing) - ✅ System functions CRUD (permission management) - ✅ Email account management (multi-account per employee) - ✅ Keycloak SSO integration (auth.lab.taipei) - ✅ Redis session storage (10.1.0.254:6379) - Solves Cookie 4KB limitation - Cross-system session sharing - Sliding expiration (8 hours) - Automatic token refresh Technical Stack: Backend: - FastAPI + SQLAlchemy - PostgreSQL 16 (10.1.0.20:5433) - Keycloak Admin API integration - Docker Mailserver integration (SSH) - Alembic migrations Frontend: - Next.js 14 (App Router) - NextAuth 4 with Keycloak Provider - Redis session storage (ioredis) - Tailwind CSS Infrastructure: - Redis 7 (10.1.0.254:6379) - Session + Cache - Keycloak 26.1.0 (auth.lab.taipei) - Docker Mailserver (10.1.0.254) Architecture Highlights: - Session管理由 Keycloak + Redis 統一控制 - 支援多系統 (HR/WebMail/Calendar/Drive/Office) 共享 session - Token 自動刷新,異質服務整合 - 未來可無縫遷移到雲端 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
124
frontend/hooks/useAuth.ts
Normal file
124
frontend/hooks/useAuth.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* 認證 Hook
|
||||
* 提供認證狀態和權限檢查功能
|
||||
*/
|
||||
import { useSession } from 'next-auth/react'
|
||||
import { hasRole, hasAnyRole, hasAllRoles, inGroup } from '@/lib/auth'
|
||||
|
||||
export function useAuth() {
|
||||
const { data: session, status } = useSession()
|
||||
|
||||
return {
|
||||
// 認證狀態
|
||||
session,
|
||||
status,
|
||||
isLoading: status === 'loading',
|
||||
isAuthenticated: status === 'authenticated',
|
||||
isUnauthenticated: status === 'unauthenticated',
|
||||
|
||||
// 用戶資訊
|
||||
user: session?.user,
|
||||
accessToken: session?.accessToken,
|
||||
error: session?.error,
|
||||
|
||||
// 權限檢查
|
||||
hasRole: (role: string) => hasRole(session, role),
|
||||
hasAnyRole: (roles: string[]) => hasAnyRole(session, roles),
|
||||
hasAllRoles: (roles: string[]) => hasAllRoles(session, roles),
|
||||
inGroup: (group: string) => inGroup(session, group),
|
||||
|
||||
// 角色檢查快捷方式
|
||||
isAdmin: hasRole(session, 'admin'),
|
||||
isHR: hasRole(session, 'hr'),
|
||||
isManager: hasRole(session, 'manager'),
|
||||
isEmployee: hasRole(session, 'employee'),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 權限保護 Hook
|
||||
* 檢查使用者是否有必要的權限,沒有則導向錯誤頁
|
||||
*/
|
||||
export function useRequireAuth(requiredRoles?: string[]) {
|
||||
const auth = useAuth()
|
||||
|
||||
if (auth.isLoading) {
|
||||
return { ...auth, isAuthorized: false }
|
||||
}
|
||||
|
||||
if (!auth.isAuthenticated) {
|
||||
// 未登入,導向登入頁
|
||||
if (typeof window !== 'undefined') {
|
||||
window.location.href = '/auth/signin'
|
||||
}
|
||||
return { ...auth, isAuthorized: false }
|
||||
}
|
||||
|
||||
if (requiredRoles && requiredRoles.length > 0) {
|
||||
// 檢查是否有任一必要角色
|
||||
const isAuthorized = auth.hasAnyRole(requiredRoles)
|
||||
if (!isAuthorized) {
|
||||
// 無權限,導向錯誤頁
|
||||
if (typeof window !== 'undefined') {
|
||||
window.location.href = '/auth/error?error=Unauthorized'
|
||||
}
|
||||
return { ...auth, isAuthorized: false }
|
||||
}
|
||||
}
|
||||
|
||||
return { ...auth, isAuthorized: true }
|
||||
}
|
||||
|
||||
/**
|
||||
* 系統權限 Hook
|
||||
* 檢查使用者對特定系統的存取權限
|
||||
*/
|
||||
export function useSystemPermission(systemName?: string) {
|
||||
const auth = useAuth()
|
||||
|
||||
if (!systemName || !auth.user) {
|
||||
return {
|
||||
hasAccess: false,
|
||||
accessLevel: null,
|
||||
isAdmin: false,
|
||||
isUser: false,
|
||||
isReadonly: false,
|
||||
}
|
||||
}
|
||||
|
||||
// 從用戶的 groups 中解析系統權限
|
||||
// 格式: /systems/{system_name}/{access_level}
|
||||
const systemGroups = auth.user.groups?.filter((group: string) =>
|
||||
group.startsWith(`/systems/${systemName}/`)
|
||||
)
|
||||
|
||||
if (!systemGroups || systemGroups.length === 0) {
|
||||
return {
|
||||
hasAccess: false,
|
||||
accessLevel: null,
|
||||
isAdmin: false,
|
||||
isUser: false,
|
||||
isReadonly: false,
|
||||
}
|
||||
}
|
||||
|
||||
// 取得最高權限
|
||||
const accessLevels = systemGroups.map((group: string) => {
|
||||
const parts = group.split('/')
|
||||
return parts[parts.length - 1]
|
||||
})
|
||||
|
||||
const hasAdmin = accessLevels.includes('admin')
|
||||
const hasUser = accessLevels.includes('user')
|
||||
const hasReadonly = accessLevels.includes('readonly')
|
||||
|
||||
const accessLevel = hasAdmin ? 'admin' : hasUser ? 'user' : 'readonly'
|
||||
|
||||
return {
|
||||
hasAccess: true,
|
||||
accessLevel,
|
||||
isAdmin: hasAdmin,
|
||||
isUser: hasUser || hasAdmin,
|
||||
isReadonly: hasReadonly || hasUser || hasAdmin,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user