Files
crewli/apps/admin/src/layouts/components/UserProfile.vue
bert.hausmans 513ca519b2 security: migrate auth tokens to httpOnly cookies (hybrid bearer token approach)
Backend:
- CookieBearerToken middleware reads httpOnly cookie and injects Authorization
  header before Sanctum validates (prepended to API middleware group)
- SetAuthCookie trait provides cookie creation/expiry helpers with per-app
  cookie names (crewli_admin_token, crewli_app_token, crewli_portal_token)
- LoginController sets token via Set-Cookie, removes it from JSON body
- LogoutController expires the auth cookie on logout
- AuthRefreshController (POST /auth/refresh) rotates tokens with new cookie
- InvitationController accept also sets token via cookie, not JSON body
- All cookies: httpOnly, SameSite=Strict, Secure (in production)

Frontend (all three SPAs):
- Removed all localStorage token storage (apps/app, apps/portal)
- Removed all JS-readable cookie token storage (apps/admin)
- Removed Authorization: Bearer header interceptors from axios
- Auth stores now rely on GET /auth/me to validate httpOnly cookie
- Admin app: new Pinia auth store replaces useCookie-based auth pattern
- withCredentials: true ensures browser sends cookies automatically

Fixes security findings A13-1 (localStorage tokens) and A13-2 (admin
cookie flags). Tokens are now invisible to JavaScript.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 16:06:44 +02:00

157 lines
4.5 KiB
Vue

<script setup lang="ts">
import { PerfectScrollbar } from 'vue3-perfect-scrollbar'
import { useAuthStore } from '@/stores/useAuthStore'
const router = useRouter()
const ability = useAbility()
const authStore = useAuthStore()
const userData = computed(() => authStore.user)
const logout = async () => {
await authStore.logout()
// Reset ability to initial ability
ability.update([])
// Redirect to login page
await router.push('/login')
}
const userProfileList = [
{ type: 'divider' },
{ type: 'navItem', icon: 'tabler-user', title: 'Profile', to: { name: 'apps-user-view-id', params: { id: 21 } } },
{ type: 'navItem', icon: 'tabler-settings', title: 'Settings', to: { name: 'pages-account-settings-tab', params: { tab: 'account' } } },
{ type: 'navItem', icon: 'tabler-file-dollar', title: 'Billing Plan', to: { name: 'pages-account-settings-tab', params: { tab: 'billing-plans' } }, badgeProps: { color: 'error', content: '4' } },
{ type: 'divider' },
{ type: 'navItem', icon: 'tabler-currency-dollar', title: 'Pricing', to: { name: 'pages-pricing' } },
{ type: 'navItem', icon: 'tabler-question-mark', title: 'FAQ', to: { name: 'pages-faq' } },
]
</script>
<template>
<VBadge
v-if="userData"
dot
bordered
location="bottom right"
offset-x="1"
offset-y="2"
color="success"
>
<VAvatar
size="38"
class="cursor-pointer"
:color="!(userData && userData.avatar) ? 'primary' : undefined"
:variant="!(userData && userData.avatar) ? 'tonal' : undefined"
>
<VImg
v-if="userData && userData.avatar"
:src="userData.avatar"
/>
<VIcon
v-else
icon="tabler-user"
/>
<!-- SECTION Menu -->
<VMenu
activator="parent"
width="240"
location="bottom end"
offset="12px"
>
<VList>
<VListItem>
<div class="d-flex gap-2 align-center">
<VListItemAction>
<VBadge
dot
location="bottom right"
offset-x="3"
offset-y="3"
color="success"
bordered
>
<VAvatar
:color="!(userData && userData.avatar) ? 'primary' : undefined"
:variant="!(userData && userData.avatar) ? 'tonal' : undefined"
>
<VImg
v-if="userData && userData.avatar"
:src="userData.avatar"
/>
<VIcon
v-else
icon="tabler-user"
/>
</VAvatar>
</VBadge>
</VListItemAction>
<div>
<h6 class="text-h6 font-weight-medium">
{{ userData.name || userData.fullName || userData.username }}
</h6>
<VListItemSubtitle class="text-capitalize text-disabled">
{{ userData.role }}
</VListItemSubtitle>
</div>
</div>
</VListItem>
<PerfectScrollbar :options="{ wheelPropagation: false }">
<template
v-for="item in userProfileList"
:key="item.title"
>
<VListItem
v-if="item.type === 'navItem'"
:to="item.to"
>
<template #prepend>
<VIcon
:icon="item.icon"
size="22"
/>
</template>
<VListItemTitle>{{ item.title }}</VListItemTitle>
<template
v-if="item.badgeProps"
#append
>
<VBadge
rounded="sm"
class="me-3"
v-bind="item.badgeProps"
/>
</template>
</VListItem>
<VDivider
v-else
class="my-2"
/>
</template>
<div class="px-4 py-2">
<VBtn
block
size="small"
color="error"
append-icon="tabler-logout"
@click="logout"
>
Logout
</VBtn>
</div>
</PerfectScrollbar>
</VList>
</VMenu>
<!-- !SECTION -->
</VAvatar>
</VBadge>
</template>