Identity

Account

Gives signed-in users an account area where they change their profile, password, passkeys and API tokens.

Account gives every signed-in user a place to manage themselves: their name and avatar, email address, password, passkeys and API tokens. Other modules add their own pages to the same area, such as membership or tickets. Install it when users sign in to your app and should look after their own account. Staff manage other people's accounts in the admin area of Users instead.

The package is @frigear-nu/account. It is a settings page inside your public site, with its own section navigation. It owns no tables and registers no server routes: its pages call the routes of Users and Auth.

What you get

  • Pages (each opt-in): an overview at /account, /account/profile, /account/security (password and passkeys), /account/documents and /account/api-tokens.
  • Layout (on by default): the account layout, with the site header and footer, a left section list and a PageSectionSwitcher on narrow screens. Pages other modules add under /account get it too.
  • Composables and helpers: useAccount() for the profile, email and password changes, useAccountNav() for the navigation, and the AccountHeader, AccountIdentity and AccountMenu components (on by default).
  • Account menu (on by default): AccountMenu, the signed-in user's dropdown with the account links, sign-out and colour mode.

The screens follow the UX patterns and pages.

ServerClientMobile
Installed for you:@frigear-nu/module-kit, @frigear-nu/auth, @frigear-nu/users and @frigear-nu/ui, which brings @nuxt/ui.You add:@nuxthub/core.Optional:@frigear-nu/menu (the account navigation, the dropdown and the overview page), @frigear-nu/widget (the account.overview widget area) and @frigear-nu/authorization (closes the area to visitors).

Quick setup

Install Account

pnpm add @frigear-nu/account @nuxthub/core

Add it to nuxt.config.ts

Turn on the pages you want. Each takes true for its default path, or a path of your own:

nuxt.config.ts
export default defineNuxtConfig({
  modules: [
    '@nuxthub/core',
    '@frigear-nu/menu', // optional
    '@frigear-nu/account',
  ],
  account: {
    settings: {
      pages: {
        profile: true, // or a custom path, e.g. '/me'
        security: true,
        index: true, // the overview, only registered with @frigear-nu/menu
        documents: true,
        apiTokens: true,
      },
      documents: [
        { id: 'handbook', label: { da: 'Håndbog', en: 'Handbook' }, path: '/assets/files/handbook.pdf' },
        { id: 'crew', label: 'Crew plan', path: '/assets/files/crew.pdf', permission: 'crew-area:view' },
      ],
    },
  },
})
What you should see: signed in, /account/profile shows your name and email inside the site, with the account sections in a list on the left.

Options

Options go under the account key.

OptionTypeDefaultPurpose
settings.layoutbooleantrueRenders the account area in the account layout. See Layout.
settings.pages.indexstring | booleanoffThe overview: summary, widgets and shortcuts. true means /account.
settings.pages.profilestring | booleanoffName, avatar and email. true means /account/profile.
settings.pages.securitystring | booleanoffPassword, passkeys and, with OAuth Server, Signed-in apps. true means /account/security.
settings.pages.documentsstring | booleanoffLists settings.documents for download. true means /account/documents.
settings.pages.apiTokensstring | booleanoffCreate and revoke API tokens. true means /account/api-tokens.
settings.documentsAccountDocument[][]Files offered on the documents page.

settings.layout. An app/layouts/account.vue of your own (or a layer's) replaces the module's, and the route rule still applies it by name. false registers no layout and sets no rule. The shipped pages then render their own AccountHeader but bring no container.

settings.pages.index. It needs @frigear-nu/menu. Without Menu it is skipped with a warning.

settings.pages.apiTokens. It is skipped when Auth has apiTokens: false.

settings.pages.*. A page is on with true (its default path) or a custom path, and off when left out. A page whose path already exists in your app is not registered. So your app replaces a shipped page by declaring its own at that path.

type AccountDocument = {
  id: string
  /** A string, or a locale map (falls back to `en`, then the first value). */
  label: string | Record<string, string>
  /** Where the file is served from, e.g. `/assets/files/handbook.pdf`. */
  path: string
  icon?: string
  /** The permission a user must hold to see it listed. Omit to list it for every signed-in user. Needs Authorization. */
  permission?: string
}
permission only filters the list, and without Authorization a document with a permission is never listed. A file served from public/ can be fetched by anyone who knows its URL. Put sensitive files behind a server route if that matters.

Server

Account registers no server routes of its own. Its pages call the self-service routes of Users through usersApi: PATCH /api/users/me (profile), PUT /api/users/me/email and PUT /api/users/me/password. They need a cookie session and answer the caller's profile.

App

The pages, the layout and the components your app can use or replace.

Pages

API tokens. The page lists the user's tokens in a table.

  • Create API token, in the page heading's actions, opens a slideover with the name, scopes and expiry.
  • The new token is shown once above the table, with a copy button.
  • Revoking a token asks for confirmation.
  • Tokens are created through Auth.
  • When users.settings.mcp is on, the page also shows a "Connect to MCP" card (MCP).

Profile and security. The profile page opens Change email in a slideover. The security page's Add passkey, in the passkeys card heading, opens a slideover too, with an optional name field.

Email changes. Confirmed email changes are a setting of Users (users.settings.verifyEmailChange). Point users.settings.profilePath at the profile page, which then shows the result. See Users.

Layout

Every page under /account and /<locale>/account renders in the account layout, plus any configured page path outside it.

  • The module sets this with an appLayout route rule. So pages other modules add under /account (membership, tickets, applications) get the layout too, whenever they register.
  • A page that sets layout in its own definePageMeta keeps its choice.

The layout itself (site header and footer, a left section list, PageSectionSwitcher on narrow screens) is described in Account area. The navigation is built from the account menu, and the layout renders no heading.

Details:

  • Without @frigear-nu/menu, the navigation lists the pages that are on in settings.pages.
  • An app without a default layout still works: the area renders on its own.
  • The document title defaults to the active navigation entry. A page's heading overrides it.
  • app.vue must render <NuxtLayout> for any layout to apply.

Heading

Every account page renders its own heading. AccountHeader is an opt-in PageHeading for this. Its title is the page's title meta, else the label of the account menu item the route belongs to (longest matching path), else "Account". It also sets the document title.

Overview

The overview (pages.index) shows, in order:

  1. an AccountHeader ("Overview"),
  2. a summary card: UUser with avatar, name and email, and AccountIdentity,
  3. the account.overview widget area, when @frigear-nu/widget is installed,
  4. a card with shortcuts to every other entry of the account menu.

Widgets receive the session user as a prop. A module adds one from its setup():

// In another module's setup()
import { extendWidgets } from '@frigear-nu/widget/utils'

extendWidgets(nuxt, 'account.overview', {
  id: 'next-ticket',
  component: resolver.resolve('./runtime/app/components/NextTicket.vue'),
  order: 10,
})

Use StatCard for numbers and ListCard for short lists, as on the admin dashboard.

Composables

const { updateProfileDetails, changeEmailAddress, changePassword, pending, error } = useAccount()

await updateProfileDetails({ name: 'Ada Lovelace', avatar: 'https://example.com/ada.png' }) // avatar: null clears it
await changeEmailAddress({ email: 'ada@example.com' })
await changePassword({ currentPassword: 'old', newPassword: 'new-password', confirmNewPassword: 'new-password' })

Each call refreshes the user session afterwards. pending and error are refs.

const { items, links, current } = await useAccountNav()

The layout, the heading and the overview read useAccountNav():

  • items are the account navigation entries: the account menu, or the pages that are on without the Menu module.
  • links are the same entries as UNavigationMenu items, localised and with active.
  • current is the entry the route belongs to.

The module also auto-imports the components AccountHeader, AccountIdentity and AccountMenu.

AccountMenu

The signed-in user's dropdown. It shows:

  • the user, with a role badge for any role other than user,
  • the account.dropdown menu: Account, Admin and sign-out,
  • Appearance: light, dark or system, which sets useColorMode().preference.

Items with a permission are only listed for users who hold it (Authorization filters them through the menus:filter hook). The entry for the area you are in is left out. Items with order 99 or more (sign-out) come last.

PropTypePurpose
blockbooleanA full-width row (avatar, name, chevrons) for a sidebar footer, as in the admin shell. Without it, an avatar button for a site header.
collapsedbooleanWith block: only the avatar, for a collapsed sidebar.
<AccountMenu />
<AccountMenu block :collapsed="collapsed" />

The colour mode lives here rather than in the header. Set header.colorMode: false in the base layer app config to drop the header toggle.

Overriding parts

The module's components have a lower priority than your app's, so a component with the same name in your app replaces it. AccountIdentity is shown next to the user in the overview's summary card and nowhere else. By default it is a role badge for anyone who is not a plain user, otherwise nothing. Override it to show something else, such as a member number or a Frigear ID badge:

<!-- app/components/Account/AccountIdentity.vue -->
<template>
  <MyIdentityDetails />
</template>

With Menu installed, each page that is on adds an item. The account menu is the account navigation and the overview's shortcuts. account.dropdown is AccountMenu. order sorts within the menu.

ItemMenuOrderPage
account ("Account")account.dropdown-99pages.index
account-overview ("Overview")account0pages.index
profileaccount and account.dropdown10pages.profile
securityaccount20pages.security
api-tokensaccount25pages.apiTokens
documentsaccount30pages.documents

Other modules add to the same menu: Membership (Subscriptions) at 15, Tickets (Events) at 40 and Applications (OAuth Server) at 50. Pick an order between these for a new page. The source is registered for the account menu.

Signed-in only

With @frigear-nu/authorization installed, the module adds an authenticated: true route rule for /account and /account/**, and for each /<locale> prefix. So every page under it is closed to visitors, including the pages other modules add (membership, tickets, applications).

  • Set authorization.redirectTo to send visitors to the sign-in page instead of a 401 error.
  • Without the Authorization module, the pages are not guarded and the module logs a warning.

Routes

None. The module registers no server routes.

Translations

Override any string in your app's i18n/locales/<code>.json. The module registers da and en locale files under the account.* namespace. See overriding module strings.

  • The key groups are title, menu.*, profile.*, documents.* and security.*.
  • The layout and the menu use nav.*, appearance.*, overview.* and accountMenu.
  • The full list is in src/runtime/i18n/lang/en.json.
  • Without @nuxtjs/i18n, the English defaults are used.
{ "account": { "profile": { "title": "My details" } } }

Next steps

Copyright © 2026