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>
157 lines
4.5 KiB
Vue
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>
|