guardia-messenger/hooks/useRoleMenu.ts

101 lines
3.2 KiB
TypeScript

/**
* #32 역할별 메뉴 제어 훅
* JWT payload에서 role을 추출해 노출 허용 탭 목록을 반환한다.
*
* engineer: 운영 탭 / pm: 승인+보고 / admin: 전체
*/
import { useEffect, useState } from 'react'
import * as SecureStore from 'expo-secure-store'
export type Role = 'engineer' | 'pm' | 'admin' | string
/** 실제 (tabs)에 존재하는 라우트 기준 매핑 */
export const ROLE_TABS: Record<string, string[]> = {
engineer: ['index', 'sr', 'chat', 'notifications', 'dr', 'network', 'scan', 'settings'],
pm: ['index', 'sr', 'chat', 'notifications', 'insights', 'settings'],
admin: ['*'], // 전체 허용
}
/** 모든 탭 (admin/fallback 용) */
export const ALL_TABS = [
'index', 'sr', 'chat', 'notifications', 'dr', 'network',
'insights', 'voice', 'scan', 'settings',
]
/** RN-safe base64 디코더 (atob 미보장 환경 대응) */
const B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
function base64UrlDecode(input: string): string {
let str = input.replace(/-/g, '+').replace(/_/g, '/')
while (str.length % 4) str += '='
let output = ''
for (let i = 0; i < str.length; ) {
const e1 = B64.indexOf(str.charAt(i++))
const e2 = B64.indexOf(str.charAt(i++))
const e3 = B64.indexOf(str.charAt(i++))
const e4 = B64.indexOf(str.charAt(i++))
const c1 = (e1 << 2) | (e2 >> 4)
const c2 = ((e2 & 15) << 4) | (e3 >> 2)
const c3 = ((e3 & 3) << 6) | e4
output += String.fromCharCode(c1)
if (e3 !== 64 && e3 !== -1) output += String.fromCharCode(c2)
if (e4 !== 64 && e4 !== -1) output += String.fromCharCode(c3)
}
// UTF-8 디코드
try {
return decodeURIComponent(
output.split('').map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)).join('')
)
} catch {
return output
}
}
export function decodeJWT(token: string): { role?: string; user_id?: string; sub?: string; [k: string]: any } {
try {
const parts = token.split('.')
if (parts.length < 2) return {}
return JSON.parse(base64UrlDecode(parts[1]))
} catch {
return {}
}
}
/** 토큰에서 role만 동기적으로 추출 (토큰 문자열을 이미 가진 경우) */
export function roleFromToken(token: string | null | undefined): Role {
if (!token) return 'engineer'
const p = decodeJWT(token)
return (p.role as string) ?? 'engineer'
}
/** role 기준 허용 탭 목록 */
export function tabsForRole(role: Role): string[] {
const allowed = ROLE_TABS[role]
if (!allowed || allowed.includes('*')) return ALL_TABS
return allowed
}
/** SecureStore 토큰을 읽어 허용 탭을 반환하는 비동기 훅 */
export function useRoleMenu(): { role: Role; tabs: string[]; loading: boolean } {
const [role, setRole] = useState<Role>('engineer')
const [tabs, setTabs] = useState<string[]>(ALL_TABS)
const [loading, setLoading] = useState(true)
useEffect(() => {
;(async () => {
try {
const token = await SecureStore.getItemAsync('grd_token')
const r = roleFromToken(token)
setRole(r)
setTabs(tabsForRole(r))
} catch {
setRole('engineer')
setTabs(tabsForRole('engineer'))
} finally {
setLoading(false)
}
})()
}, [])
return { role, tabs, loading }
}