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>
125 lines
3.1 KiB
TypeScript
125 lines
3.1 KiB
TypeScript
/**
|
|
* 認證 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,
|
|
}
|
|
}
|