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
authslot in the global router-middleware chain ismiddleware/redirect.js, not the built-in@nuxtjs/authmiddleware. 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 rootuserstate, and the Vuexapi/userDetailsCache. They are not always consistent.
Cookie inventory¶
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_urlis server-driven. The backend tells the frontend where to send the user viagetaccountdetails.data[0].landing_url. That's why a new account with onboarding incomplete is sent to e.g./<accountId>/Organisationprofileinstead of/<accountId>/contentplanner.onboardingTypeIdcontrols which onboarding flow is loaded. Persisted in theaccountdetailscookie and re-read by onboarding pages.- A pending Paddle txn supersedes everything. If
paddleTxnIdis set andfetchSubscription()returns nosub_id, the user is redirected to the payment page on every navigation until either the cookie is cleared or a sub appears. getuseris called twice on the happy path (once becauseusedetailis present, again becauseuseid && $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. |
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:
- Don't duplicate
getuser's logic. If you find yourself readingaccountId, fetching account details, and redirecting somewhere, you almost certainly want to extendmiddleware/redirect.jsinstead. - 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 inredirect.js. - 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. impersonationLoginis 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.
Per-page middleware: auth: false and sociallink¶
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:
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.jsreadsauth._token.facebookandauth._token.instagramcookies that don't appear to be set anywhere in this repo (no Facebook/Instagram strategies innuxt.config.js). It is plausibly vestigial. Verify with the team before relying on or removing.- The
cookie-universal-nuxtlibrary we use does not persist cookies across cross-site iframes by default; thesameSite: '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: falsemeans$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).