Pulled ALL skills from 15 source repositories: - anthropics/skills: 16 (docs, design, MCP, testing) - obra/superpowers: 14 (TDD, debugging, agents, planning) - coreyhaines31/marketingskills: 25 (marketing, CRO, SEO, growth) - better-auth/skills: 5 (auth patterns) - vercel-labs/agent-skills: 5 (React, design, Vercel) - antfu/skills: 16 (Vue, Vite, Vitest, pnpm, Turborepo) - Plus 13 individual skills from various repos Mosaic Stack is not limited to coding — the Orchestrator and subagents serve coding, business, design, marketing, writing, logistics, analysis, and more. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
168 lines
4.8 KiB
Markdown
168 lines
4.8 KiB
Markdown
---
|
|
title: Per-Route beforeEnter Guards Ignore Param/Query Changes
|
|
impact: MEDIUM
|
|
impactDescription: Route-level beforeEnter guards don't fire when only params, query, or hash change, causing unexpected bypasses of validation logic
|
|
type: gotcha
|
|
tags: [vue3, vue-router, navigation-guards, params, query]
|
|
---
|
|
|
|
# Per-Route beforeEnter Guards Ignore Param/Query Changes
|
|
|
|
**Impact: MEDIUM** - The `beforeEnter` guard defined in route configuration only triggers when entering a route from a DIFFERENT route. Changes to params, query strings, or hash within the same route do NOT trigger `beforeEnter`, potentially bypassing important validation logic.
|
|
|
|
## Task Checklist
|
|
|
|
- [ ] Use in-component `onBeforeRouteUpdate` for param/query changes
|
|
- [ ] Or use global `beforeEach` with route.params/query checks
|
|
- [ ] Document which guards protect which scenarios
|
|
- [ ] Test navigation between same route with different params
|
|
|
|
## The Problem
|
|
|
|
```javascript
|
|
// router.js
|
|
const routes = [
|
|
{
|
|
path: '/orders/:id',
|
|
component: OrderDetail,
|
|
beforeEnter: async (to, from) => {
|
|
// This runs when entering from /products
|
|
// But NOT when navigating from /orders/1 to /orders/2!
|
|
const order = await checkOrderAccess(to.params.id)
|
|
if (!order.canView) {
|
|
return '/unauthorized'
|
|
}
|
|
}
|
|
}
|
|
]
|
|
```
|
|
|
|
**Scenario:**
|
|
1. User navigates from `/products` to `/orders/1` - beforeEnter runs, access checked
|
|
2. User navigates from `/orders/1` to `/orders/2` - beforeEnter DOES NOT run!
|
|
3. User might access order they don't have permission for!
|
|
|
|
## What Triggers beforeEnter vs. What Doesn't
|
|
|
|
| Navigation | beforeEnter fires? |
|
|
|------------|-------------------|
|
|
| `/products` → `/orders/1` | YES |
|
|
| `/orders/1` → `/orders/2` | NO |
|
|
| `/orders/1` → `/orders/1?tab=details` | NO |
|
|
| `/orders/1#section` → `/orders/1#other` | NO |
|
|
| `/orders/1` → `/products` → `/orders/2` | YES (leaving and re-entering) |
|
|
|
|
## Solution 1: Add In-Component Guard
|
|
|
|
```vue
|
|
<!-- OrderDetail.vue -->
|
|
<script setup>
|
|
import { onBeforeRouteUpdate } from 'vue-router'
|
|
|
|
// Handle param changes within the same route
|
|
onBeforeRouteUpdate(async (to, from) => {
|
|
if (to.params.id !== from.params.id) {
|
|
const order = await checkOrderAccess(to.params.id)
|
|
if (!order.canView) {
|
|
return '/unauthorized'
|
|
}
|
|
}
|
|
})
|
|
</script>
|
|
```
|
|
|
|
## Solution 2: Use Global beforeEach Instead
|
|
|
|
```javascript
|
|
// router.js
|
|
router.beforeEach(async (to, from) => {
|
|
// Handle all order access checks globally
|
|
if (to.name === 'OrderDetail') {
|
|
// This runs on EVERY navigation to this route, including param changes
|
|
const order = await checkOrderAccess(to.params.id)
|
|
if (!order.canView) {
|
|
return '/unauthorized'
|
|
}
|
|
}
|
|
})
|
|
```
|
|
|
|
## Solution 3: Combine Both Guards
|
|
|
|
```javascript
|
|
// router.js - For entering from different route
|
|
const routes = [
|
|
{
|
|
path: '/orders/:id',
|
|
component: OrderDetail,
|
|
beforeEnter: (to) => validateOrderAccess(to.params.id)
|
|
}
|
|
]
|
|
|
|
// In component - For param changes within route
|
|
// OrderDetail.vue
|
|
onBeforeRouteUpdate((to) => validateOrderAccess(to.params.id))
|
|
|
|
// Shared validation function
|
|
async function validateOrderAccess(orderId) {
|
|
const order = await checkOrderAccess(orderId)
|
|
if (!order.canView) {
|
|
return '/unauthorized'
|
|
}
|
|
}
|
|
```
|
|
|
|
## Solution 4: Use beforeEnter with Array of Guards
|
|
|
|
```javascript
|
|
// guards/orderGuards.js
|
|
export const orderAccessGuard = async (to) => {
|
|
const order = await checkOrderAccess(to.params.id)
|
|
if (!order.canView) {
|
|
return '/unauthorized'
|
|
}
|
|
}
|
|
|
|
// router.js
|
|
const routes = [
|
|
{
|
|
path: '/orders/:id',
|
|
component: OrderDetail,
|
|
beforeEnter: [orderAccessGuard] // Can add multiple guards
|
|
}
|
|
]
|
|
|
|
// Still need in-component guard for param changes!
|
|
```
|
|
|
|
## Full Navigation Guard Execution Order
|
|
|
|
Understanding when each guard type fires:
|
|
|
|
```
|
|
1. beforeRouteLeave (in-component, leaving component)
|
|
2. beforeEach (global)
|
|
3. beforeEnter (per-route, ONLY when entering from different route)
|
|
4. beforeRouteEnter (in-component, entering component)
|
|
5. beforeResolve (global)
|
|
6. afterEach (global, after navigation confirmed)
|
|
|
|
For param/query changes on same route:
|
|
1. beforeRouteUpdate (in-component) - ONLY this fires!
|
|
2. beforeEach (global)
|
|
3. beforeResolve (global)
|
|
4. afterEach (global)
|
|
```
|
|
|
|
## Key Points
|
|
|
|
1. **beforeEnter is for route ENTRY only** - Not for within-route changes
|
|
2. **Use onBeforeRouteUpdate for param changes** - This is the in-component solution
|
|
3. **Global beforeEach always runs** - Good for centralized validation
|
|
4. **Test param change scenarios** - Easy to miss during development
|
|
5. **Consider security implications** - Param-based access control needs both guards
|
|
|
|
## Reference
|
|
- [Vue Router Navigation Guards](https://router.vuejs.org/guide/advanced/navigation-guards.html)
|
|
- [Vue Router Per-Route Guards](https://router.vuejs.org/guide/advanced/navigation-guards.html#per-route-guard)
|