101 lines
3.2 KiB
TypeScript
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 }
|
|
}
|