Skip to content

03 — Auth & session lifecycle

This is the most load-bearing flow in the codebase and the one most likely to bite you. Read it before touching anything related to login, account switching, redirects, or onboarding.

TL;DR

  • @nuxtjs/auth (local strategy) handles login/token refresh, but the source of truth for identity is cookies, not the auth strategy.
  • The auth slot in the global router-middleware chain is middleware/redirect.js, not the built-in @nuxtjs/auth middleware. Despite its filename, that file is the post-login decision tree for the entire app.
  • Failure modes are silent: most error branches end with app.$auth.logout() + cookies.removeAll() + localStorage.clear() + redirect('/#login').
  • Identity is read from at least four places — cookies, app.$auth.user, Vuex root user state, and the Vuex api/userDetailsCache. They are not always consistent.

Each of these is set or read somewhere in the app. The list is the union of what store/index.js, middleware/redirect.js, middleware/axios.js, and the social-login flows touch.

Cookie Set by Read by Meaning
token getUserData, socialRegister, linkedInLogin, twitterLogin (in store/index.js) middleware/axios.js (request header on /auth/*) The bearer token returned by the backend on login. Authoritative for outbound API calls.
usedetail All login actions in store/index.js middleware/redirect.js, middleware/axios.js The full user-detail object. Contains memberId/id, accountId, email, etc.
accountId socialLinkMe (store/index.js line ~426) appendUrl middleware, store actions, server requests The currently active account ID. Shadowed by usedetail.accountId; use this for URL construction.
useid All login actions middleware/redirect.js, posteditor save The user's member/user ID. Often duplicates usedetail.memberId / usedetail.id.
username All login actions UI display The display name (name.full).
email All login actions Login flow restoration Email used for $auth.loginWith('local', …) re-login.
avatar socialLinkMe UI Avatar URL.
userLogin All login actions Various Boolean — "is this a real user login (not impersonation)".
impersonationLogin All login actions (set to false); admin impersonation flow sets to true middleware/redirect.js, getUTZ Boolean — "is this an admin impersonating a user". Suppresses some redirects and profile updates.
impersonredirect Impersonation flow middleware/redirect.js One-shot flag to force redirect into /<accountId>/contentplanner after impersonation.
socialAccConnect Social-account connection flow middleware/redirect.js One-shot flag to force redirect into /<accountId>/userSetting#social_media_accounts.
socialAuth LinkedIn, Twitter login middleware/redirect.js Boolean — "the most recent auth attempt was a social re-link". Triggers a logout-if-not-loggedIn.
currentUrl middleware/axios.js on every nav middleware/redirect.js Last navigated path+hash. Used to restore the user's intended destination after login.
accountdetails middleware/redirect.js → getuser UI / onboarding Cached subset of getaccountdetails response (isInvited, noId, roleType, accountId, onboardingTypeId).
paddleTxnId Paddle checkout flow middleware/redirect.js A pending Paddle transaction. Triggers a redirect to /<accountId>/payment if no active sub.
subs store/api.js → fetchSubscription UI Cached { sub_id } of the active subscription.
registerUser Social registration flow plugins/auth.js { status, path } — one-shot redirect target after social signup.
auth._token.facebook @nuxtjs/auth (Facebook strategy?) plugins/auth.js Verify with the team — referenced in plugins/auth.js but no Facebook auth strategy is defined in nuxt.config.js. May be legacy.
auth._token.instagram Same as above plugins/auth.js Same as above.
sconnect Verify — only read in middleware/redirect.js, set site unclear middleware/redirect.js Triggers an automatic re-login attempt with stored credentials when not logged in.
ReLogin (Set site commented out) Vestigial. Safe to ignore.
modalImageReload posteditor save UI listeners Trigger flag for image-modal refresh.
fbconn Facebook OAuth flow middleware/sociallink.js Marker that a Facebook connect roundtrip just completed; gate for the social-link RSVP.all dispatch.
current_path middleware/socialLogin.js The same middleware Last navigated path — the social-login middleware uses it to detect post-OAuth landing on /#login. (Not to be confused with currentUrl.)

The @nuxtjs/auth strategy uses sameSite: 'none'; secure: true (see nuxt.config.js → auth.cookie.options). Local development on plain http://localhost will fail to set these cookies — see 01-local-setup.md.

localStorage also holds a few identity-shaped values (accountId, loggedInUserId, profiletab, utm_*, fallback-path). These are used by the editor (which lives in a different React tree) and by appendUrl/appendUTM middleware.

The post-login decision tree (middleware/redirect.js)

This file runs on every navigation as the first router middleware (declared as auth in nuxt.config.js, but not the built-in auth middleware). It does the following, in order:

┌─ if !$auth.loggedIn AND cookie 'sconnect' is truthy
│   └─ attempt $auth.loginWith('local', { …, social: true })       ← silent re-login
├─ resolve a user id from cookies: useid OR usedetail.memberId OR usedetail.id
├─ if cookie 'usedetail' present
│   └─ getuser(app, redirect)                                       ← see below
├─ if userId AND $auth.loggedIn
│   └─ getuser(app, redirect)                                       ← (called twice in the happy path)
└─ else
    └─ setTimeout(1s) → checkLinkLogin → either getuser(...) or logout-and-redirect-to-login

getuser is the real decision logic:

getuser:
  accountId = $auth.user.accountId
  Promise.all([
    store.dispatch('plan/fetchUserDetails'),
    $axios.post('/auth/getaccountdetails', { account_id: accountId })
  ])

  if response has data:
    if NOT impersonating:
      if cookie 'paddleTxnId' present:
        sub = store.dispatch('api/fetchSubscription')
        if sub.sub_id:  remove the cookie (the txn already became a real sub)
        else:           redirect /<accountId>/payment            ← STOP

      if currentUrl in [/social_accounts, /userSetting#profile]:
        redirect to currentUrl                                   ← restore intent
      else if landing_url is set AND we are not already on it:
        cookies.set('accountdetails', { …subset of response… })
        store.dispatch('onboarding/fetchOnboardingFlow', { obId, accountId })
        redirect /<accountId><landing_url>                       ← onboarding-aware landing
      else:
        redirect /<accountId>/contentplanner                     ← default landing

    else if 'impersonredirect':
      cookies.set('impersonredirect', false)
      cookies.set('accountdetails', { … })
      redirect /<accountId>/contentplanner
    else if 'socialAccConnect':
      cookies.set('socialAccConnect', false)
      redirect /<accountId>/userSetting#social_media_accounts

  else (no data → bad session):
    $auth.logout(); cookies.removeAll(); localStorage.clear(); redirect /#login

  on any thrown error:
    $auth.logout(); cookies.removeAll(); localStorage.clear(); redirect /#login

A few non-obvious consequences:

  • landing_url is server-driven. The backend tells the frontend where to send the user via getaccountdetails.data[0].landing_url. That's why a new account with onboarding incomplete is sent to e.g. /<accountId>/Organisationprofile instead of /<accountId>/contentplanner.
  • onboardingTypeId controls which onboarding flow is loaded. Persisted in the accountdetails cookie and re-read by onboarding pages.
  • A pending Paddle txn supersedes everything. If paddleTxnId is set and fetchSubscription() returns no sub_id, the user is redirected to the payment page on every navigation until either the cookie is cleared or a sub appears.
  • getuser is called twice on the happy path (once because usedetail is present, again because useid && $auth.loggedIn). This is intentional in the current code, but means redirects can fire twice — be careful when adding side effects.

Where identity is read across the app

This is the part that surprises new contributors. There are four overlapping sources:

Source Truthful when? Use it for
app.$cookies.get('usedetail') Always, after login The pragmatic source of truth. Most middleware reads from here.
app.$auth.user After @nuxtjs/auth has fetched me Inside getuser and any post-login flow. May be null mid-navigation.
store.state.user (root) Only if getUserData action ran Rarely populated. Don't rely on it.
store.getters['api/getUserDetails'] After api/fetchUserDetails was dispatched When you need fields from the backend's /auth/getUserdetail response (preferences, language, …).

Default rule: prefer app.$auth.user for identity in pages, fall back to cookies in middleware/server-side helpers, and use api/fetchUserDetails only when you need the richer profile fields. Never trust store.state.user to be set.

AccountSwitcher.vue is a useful reference — it watches $auth.user.accountId and keeps a local selectedAccount in sync with both cookies and Vuex.

Login flows

There are five entry paths into authenticated state. All of them ultimately call $auth.loginWith('local', …) so that the auth strategy's interceptors are active, then set the same cookie set as a side-effect.

Flow Action Notes
Email/password Direct $auth.loginWith('local', …) Standard happy path.
Social register (Google?) socialRegister action Hits /socialRegister, sets cookies, returns token. Verify with the team which providers route through here.
LinkedIn linkedInLogin action Hits /linkedInRegister, then $auth.loginWith('local', { …, social: true, sid: socialId, password: '##########' }).
Twitter (v1 OAuth) twitterLogin action Hits /twitterRegister. Same trailing loginWith('local', …) step.
Twitter (relink) twitterRLAuth action Hits /twitterAuth for already-existing accounts.

The literal placeholder password: '##########' in social-login loginWith('local', …) calls is intentional — the backend recognises the social: true flag and bypasses password verification.

Adding new auth-aware behaviour

A few rules of thumb:

  1. Don't duplicate getuser's logic. If you find yourself reading accountId, fetching account details, and redirecting somewhere, you almost certainly want to extend middleware/redirect.js instead.
  2. Use the existing one-shot cookie pattern (impersonredirect, socialAccConnect) for "redirect to X exactly once after login" requirements. Set the cookie before login, read+clear it in redirect.js.
  3. If you need to clear auth state programmatically, do all four steps: $auth.logout(), $cookies.removeAll(), localStorage.clear(), redirect('/#login'). Skipping any of them leaves the app in an inconsistent half-logged-in state.
  4. impersonationLogin is a tripwire, not a feature flag. Several actions (getUTZ, updateProfile, billing logic) early-return if it's true to avoid corrupting the impersonated user's data. Respect it.

Pages can opt out of the @nuxtjs/auth requirement entirely by setting auth: false in their script block. This is how all public/marketing pages are wired:

export default {
  auth: false,
  middleware: ['sociallink'],   // optional — see below
  // …
}

Pages that currently use auth: false: pages/index.vue (login), pages/redirect.vue, pages/callback/index.vue, pages/socialAuth/index.vue, pages/password_setup/index.vue, pages/privacy/index.vue, pages/plan/index.vue, pages/pay.vue, pages/fbDeletionStatus/index.vue, pages/sample/index.vue. Use this for any new public route — don't try to bypass auth via routing tricks.

middleware: ['sociallink'] is a separate per-page opt-in (middleware/sociallink.js) that runs the social-link reconciliation. It's wired on pages/index.vue, pages/_accid/userSetting/index.vue, pages/social_accounts/index.vue, pages/password_setup/index.vue, and pages/callback/index.vue. The middleware reads the fbconn cookie and, if present, dispatches sociallink + sociallinkstatus + socialLinkMe in parallel via RSVP.

Numeric role values

Authorization is encoded as integer fields on the user/account record. They surface in many components as bare numeric comparisons; consolidating the values here:

Field Values seen in code Meaning (best-effort — verify with the team)
roleType 1 Standard member.
roleType 5 Owner. head.js and other code gates billing/subscription redirects on this.
roleType 11 Some elevated role (referenced in components). Verify.
account_role 3 Restricted role — head.js blocks /my_library, /my_library#content, /dashboard/roi, /dashboard/overview.
isInvited 0, 1 1 = user joined via invitation. Skips owner-only billing redirects.
noId 1 Special "BNI"-style flag. helpers/helper.js → getPlanList filters out launch/$1 plans for these users.

See 12-permissions-and-roles.md for the per-role surface area.

A note on middleware/head.js

There is a fourth auth-related middleware file, middleware/head.js, that contains additional load-bearing logic: it sets localStorage.accountId from the cookie (which appendUrl middleware then reads — see 04-routing.md), syncs usedetail cookie ↔ $auth.user, dispatches api/fetchSubscription for routes that need it, and enforces role-based access (roleType === 5, account_role === 3) and invitation/payment redirects.

It is not currently referenced anywhere — not in nuxt.config.js → router.middleware, not as per-page middleware on any page. It is either dead code retained for future wiring, or it was unwired by mistake. Verify with the team before extending or deleting it. Some of its behaviour (the localStorage.accountId write in particular) is what 04-routing.md calls a sync gap — until head.js is wired or its logic moves elsewhere, the gap is real.

Known sharp edges

  • plugins/auth.js reads auth._token.facebook and auth._token.instagram cookies that don't appear to be set anywhere in this repo (no Facebook/Instagram strategies in nuxt.config.js). It is plausibly vestigial. Verify with the team before relying on or removing.
  • The cookie-universal-nuxt library we use does not persist cookies across cross-site iframes by default; the sameSite: 'none' setting in the auth strategy applies only to the auth cookies, not to the cookies the app sets via $cookies.set(...).
  • The auth strategy's logout: false means $auth.logout() is a local-only operation — it does not call any backend endpoint. There is no server-side session to invalidate; revocation must be handled by the backend (e.g. by token expiry).