fix: impersonation logout, applicant learning hub redirect, nav click tracking
- Sign Out button during impersonation now returns to admin instead of destroying the session (fixes multi-click logout issue) - Applicant nav now respects learning_hub_external setting like jury/mentor - Track Learning Hub nav clicks via audit log (LEARNING_HUB_CLICK action) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,8 @@ export function ApplicantNav({ user }: ApplicantNavProps) {
|
||||
staleTime: 60_000,
|
||||
})
|
||||
|
||||
const useExternal = featureFlags?.learningHubExternal && featureFlags.learningHubExternalUrl
|
||||
|
||||
const navigation: NavItem[] = [
|
||||
{ name: 'Dashboard', href: '/applicant', icon: Home },
|
||||
{ name: 'Project', href: '/applicant/team', icon: FolderOpen },
|
||||
@@ -27,7 +29,12 @@ export function ApplicantNav({ user }: ApplicantNavProps) {
|
||||
...(flags?.hasMentor
|
||||
? [{ name: 'Mentoring', href: '/applicant/mentor', icon: MessageSquare }]
|
||||
: []),
|
||||
{ name: 'Resources', href: '/applicant/resources', icon: BookOpen },
|
||||
{
|
||||
name: 'Learning Hub',
|
||||
href: useExternal ? featureFlags.learningHubExternalUrl : '/applicant/resources',
|
||||
icon: BookOpen,
|
||||
external: !!useExternal,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
|
||||
@@ -71,13 +71,31 @@ export function RoleNav({ navigation, roleName, user, basePath, statusBadge, edi
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false)
|
||||
const { data: session, status: sessionStatus, update: updateSession } = useSession()
|
||||
const isAuthenticated = sessionStatus === 'authenticated'
|
||||
const isImpersonating = !!session?.user?.impersonating
|
||||
const { data: avatarUrl } = trpc.avatar.getUrl.useQuery(undefined, {
|
||||
enabled: isAuthenticated,
|
||||
})
|
||||
const endImpersonation = trpc.user.endImpersonation.useMutation()
|
||||
const logNavClick = trpc.learningResource.logNavClick.useMutation()
|
||||
const { theme, setTheme } = useTheme()
|
||||
const [mounted, setMounted] = useState(false)
|
||||
useEffect(() => setMounted(true), [])
|
||||
|
||||
const handleSignOut = async () => {
|
||||
if (isImpersonating) {
|
||||
try {
|
||||
await endImpersonation.mutateAsync()
|
||||
await updateSession({ endImpersonation: true })
|
||||
window.location.href = '/admin/members'
|
||||
} catch {
|
||||
// Fallback: just sign out completely
|
||||
signOut({ callbackUrl: '/login' })
|
||||
}
|
||||
return
|
||||
}
|
||||
signOut({ callbackUrl: '/login' })
|
||||
}
|
||||
|
||||
// Auto-refresh session on mount to pick up role changes without requiring re-login
|
||||
useEffect(() => {
|
||||
if (isAuthenticated) {
|
||||
@@ -115,7 +133,14 @@ export function RoleNav({ navigation, roleName, user, basePath, statusBadge, edi
|
||||
)
|
||||
if (item.external) {
|
||||
return (
|
||||
<a key={item.name} href={item.href} target="_blank" rel="noopener noreferrer" className={className}>
|
||||
<a
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={className}
|
||||
onClick={() => logNavClick.mutate({ url: item.href })}
|
||||
>
|
||||
<item.icon className="h-4 w-4" />
|
||||
{item.name}
|
||||
<ExternalLinkIcon className="h-3 w-3 opacity-50" />
|
||||
@@ -211,11 +236,11 @@ export function RoleNav({ navigation, roleName, user, basePath, statusBadge, edi
|
||||
)}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={() => signOut({ callbackUrl: '/login' })}
|
||||
onClick={handleSignOut}
|
||||
className="text-destructive focus:text-destructive"
|
||||
>
|
||||
<LogOut className="mr-2 h-4 w-4" />
|
||||
Sign Out
|
||||
{isImpersonating ? 'Return to Admin' : 'Sign Out'}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
@@ -257,7 +282,14 @@ export function RoleNav({ navigation, roleName, user, basePath, statusBadge, edi
|
||||
)
|
||||
if (item.external) {
|
||||
return (
|
||||
<a key={item.name} href={item.href} target="_blank" rel="noopener noreferrer" onClick={() => setIsMobileMenuOpen(false)} className={className}>
|
||||
<a
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={() => { logNavClick.mutate({ url: item.href }); setIsMobileMenuOpen(false) }}
|
||||
className={className}
|
||||
>
|
||||
<item.icon className="h-4 w-4" />
|
||||
{item.name}
|
||||
<ExternalLinkIcon className="h-3 w-3 opacity-50" />
|
||||
@@ -299,10 +331,10 @@ export function RoleNav({ navigation, roleName, user, basePath, statusBadge, edi
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="w-full justify-start text-destructive hover:text-destructive"
|
||||
onClick={() => signOut({ callbackUrl: '/login' })}
|
||||
onClick={handleSignOut}
|
||||
>
|
||||
<LogOut className="mr-2 h-4 w-4" />
|
||||
Sign Out
|
||||
{isImpersonating ? 'Return to Admin' : 'Sign Out'}
|
||||
</Button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -271,6 +271,24 @@ export const learningResourceRouter = router({
|
||||
return { success: true }
|
||||
}),
|
||||
|
||||
/**
|
||||
* Log when a user clicks the Learning Hub nav link (especially external).
|
||||
* Creates an audit log entry so admins can see who accessed it.
|
||||
*/
|
||||
logNavClick: protectedProcedure
|
||||
.input(z.object({ url: z.string() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
await ctx.prisma.auditLog.create({
|
||||
data: {
|
||||
userId: ctx.user.id,
|
||||
action: 'LEARNING_HUB_CLICK',
|
||||
entityType: 'LearningResource',
|
||||
detailsJson: { url: input.url, role: ctx.user.role },
|
||||
},
|
||||
}).catch(() => {})
|
||||
return { success: true }
|
||||
}),
|
||||
|
||||
/**
|
||||
* Create a new resource (admin only)
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user