feat(cabana): add the My account page with subtitle and password change
- lagoon: 202610090001_backend_users_metadata adds WinterCMS's nullable jsonable backend_users.metadata column (TEXT); rollback drops only it - cabana.BackendUser.Metadata and Subtitle read metadata.subtitle leniently (NULL, blank, null, [], invalid JSON and non-strings read as empty) - GET /auth/me returns subtitle; PUT /account edits the signed-in admin's own names, email and subtitle, and the password with current_password; any other key is a 422 and nothing is written - email: required, valid, 6-255 chars, unique against other admins' email and login ignoring case (soft-deleted included); a unique-index race is 422 - a password change sets tokens_valid_after, revokes the presented token and gives a cookie session a fresh cookie - swag annotation, regenerated admin.json and schema.d.ts; route inventories, CSRF walk (27) and OpenAPI conformance learn the route - SPA: My account item in the user menu opens /account (AccountView), which saves through PUT /account and updates the top bar via setCurrentUser - backend::lang.myaccount strings (en, pl), rebuilt boardwalk dist - cabana and lagoon READMEs, users-and-permissions and admin-spa docs
This commit is contained in:
@@ -22,6 +22,9 @@ interface Crumb {
|
||||
const route = useRoute()
|
||||
|
||||
const crumbs = computed<Crumb[]>(() => {
|
||||
if (route.name === 'account') {
|
||||
return [{ label: t('backend::lang.myaccount.menu_label') }]
|
||||
}
|
||||
if (route.name === 'settings' || route.name === 'settings-form') {
|
||||
const out: Crumb[] = [{ label: t('backend::lang.nav.settings'), to: '/settings' }]
|
||||
const entry = route.name === 'settings-form' ? settingsEntry(String(route.params.code ?? '')) : null
|
||||
|
||||
@@ -9,14 +9,14 @@ import {
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from 'reka-ui'
|
||||
import { ChevronDown, LogOut } from '@lucide/vue'
|
||||
import { ChevronDown, LogOut, UserRound } from '@lucide/vue'
|
||||
import { t } from '../../app/i18n'
|
||||
import { currentUser, logout } from '../../state/useAuth'
|
||||
|
||||
// User menu (design: Top header): a 44px button with the initials avatar,
|
||||
// the name above the role from /auth/me (both hidden at tablet width) and a
|
||||
// chevron; the 280px menu (Reka DropdownMenu, role menu) shows the name and
|
||||
// e-mail and a Wyloguj item. Logging out always ends on the login screen,
|
||||
// e-mail, a My account item that opens /account and a Wyloguj item. Logging out always ends on the login screen,
|
||||
// even when the logout call fails (T-10-23).
|
||||
const router = useRouter()
|
||||
|
||||
@@ -47,6 +47,10 @@ const initials = computed(() => {
|
||||
|
||||
const roleName = computed(() => currentUser.value?.role?.name ?? '')
|
||||
|
||||
async function onAccount(): Promise<void> {
|
||||
await router.push({ name: 'account' }).catch(() => undefined)
|
||||
}
|
||||
|
||||
async function onLogout(): Promise<void> {
|
||||
await logout(router)
|
||||
}
|
||||
@@ -92,6 +96,14 @@ async function onLogout(): Promise<void> {
|
||||
<span class="truncate text-[13px] text-muted">{{ currentUser?.email }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<DropdownMenuItem
|
||||
data-action="account"
|
||||
class="flex h-10 cursor-pointer items-center gap-2.5 rounded-control px-3 font-medium outline-none transition-colors duration-150 ease-out data-[highlighted]:bg-hover"
|
||||
@select="onAccount"
|
||||
>
|
||||
<UserRound :size="18" aria-hidden="true" />
|
||||
<span>{{ t('backend::lang.myaccount.menu_label') }}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator class="my-1 h-px bg-border" />
|
||||
<DropdownMenuItem
|
||||
data-action="logout"
|
||||
|
||||
Reference in New Issue
Block a user