Skip to content

12 — Permissions & roles model

The frontend's authorization model is a small set of integer flags on the user/account record, compared against hardcoded lists of allowed routes and actions. This doc consolidates what those flags mean and where they're enforced.

Several values here are documented as best-effort inference from code. The backend is the source of truth for what each value really represents — confirm with the team before relying on edge cases.

The fields

Authorization is encoded across four integer-valued fields on the user/account record:

Field Type Source What it gates
roleType int $auth.user.roleType / usedetail Top-level user role (member / owner / elevated).
account_role int $auth.user.account_role Role within the current account (restrictive variant).
isInvited 0 | 1 accountdetails cookie Whether the user joined via an invite. Affects billing redirects.
noId 0 | 1 accountdetails cookie / userData "BNI cohort" flag. Affects plan filtering and likely onboarding flow.

There is no central hasPermission(action) function. Authorization is implemented as bare numeric comparisons scattered across middleware, page mount hooks, and conditional rendering blocks. The known sites are listed below.

Known values

Field Value Meaning (best-effort)
roleType 1 Standard member. Default role for users who signed up themselves.
roleType 5 Account owner. Has billing-management privileges. head.js gates payment redirects on this role.
roleType 11 An elevated role referenced in components. Verify with the team — likely admin, agency, or partner.
account_role 3 A restricted role within the account. head.js blocks /my_library, /my_library#content, /dashboard/roi, /dashboard/overview for users with this value.
isInvited 1 User joined an existing account via invitation. Skips the owner-only payment-required redirect (head.js).
isInvited 0 Default. User is treated as a primary member of their account.
noId 1 "BNI" cohort. helpers/helper.js → getPlanList filters out PLAN.LAUNCH and PLAN.ONE_DOLLAR and only shows yearly-billing or trial-eligible plans.
noId (any other) Standard plan filtering. Yearly cutoff applies.

Where each gate lives

middleware/head.js — the most centralised set of role checks

Currently unwired. head.js is not in the global middleware chain in nuxt.config.js and is not referenced as per-page middleware anywhere. Its logic is documented here because it's the most intentional consolidation of role checks in the codebase, and because the team may wire it up. Verify with the team before assuming any of this is currently effective.

head.js implements:

  • account_role === 3 block. A user with this role attempting to visit /my_library, /my_library#content, /dashboard/roi, or /dashboard/overview is redirected to /contentplanner. Effectively makes the library and the ROI dashboard owner-only surfaces.
  • roleType === 5 (owner) payment gate. If the user is an owner who is not impersonated and not invited:
  • If subscriptionData.sub_id is missing (no active sub) and the route is not in the login/onboarding path list, redirect to /payment.
  • If they do have a sub but are sitting on /billing, /topics, /Organisationprofile, /goals_objectives, /chaptername, /brandkit, or /payment of their own account — redirect to /contentplanner. (This prevents owners from re-entering onboarding once they're set up.)
// head.js (paraphrased)
if (account_role === 3 && /\/my_library|\/dashboard\/(roi|overview)/.test(route.path))
  return redirect('/contentplanner')

if (roleType === 5 && !impersonating && !isInvited) {
  if (!hasSubscription && !isOnboardingOrLoginRoute)  return redirect('/payment')
  if (hasSubscription && isOnboardingOrLoginRoute)    return redirect('/contentplanner')
}

middleware/redirect.js

  • Reads usedetail.accountId to construct the post-login redirect target.
  • Reads impersonationLogin to suppress updateProfile and the standard payment redirect.
  • Does not itself enforce role-based access — that's head.js's job (when wired).

helpers/helper.js → getPlanList

This is the only place plan-tier filtering is implemented:

const isBNI    = !!userData?.noId
const isNew    = new Date(userData.created) > new Date('2023-12-27T12:00:00Z')

filteredPlan = planList.filter(({ trialPeriod, pricing_id, billingPeriod, id, displayName }) => {
  if (isBNI)
    return (trialPeriod >= 0 || billingPeriod === YEARLY_BILLING) && id !== PLAN.LAUNCH && pricing_id !== PLAN.ONE_DOLLAR
  if (isNew)
    return trialPeriod >= 0 || displayName != 1 || id == 15 || (billingPeriod === YEARLY_BILLING && pricing_id != 16)
  return trialPeriod >= 0 || billingPeriod === YEARLY_BILLING
})

So the user's signup date and noId flag together drive a three-way branch on what plans they're allowed to choose. PLAN.LAUNCH = 7 and PLAN.ONE_DOLLAR = 28 are gated by noId; id == 15 is force-shown for new users; the cutoff date 2023-12-27T12:00:00Z is hardcoded.

Per-component checks

Components and pages also do their own bare comparisons. Greppable pattern: roleType === <n>, account_role === <n>, isInvited, noId. Some examples (non-exhaustive):

  • components/AccountSwitcher.vue — uses roleType to label account ownership.
  • Various billing pages — gate the upgrade/downgrade UI on roleType === 5.
  • helpers/helper.js → getPlanList — described above.
  • mixins/onboardingNavigation.js — implicitly assumes the onboarding flow varies by Onboarding_type_id, which is itself a function of noId/roleType (set by the backend, not the frontend).

When you add a new role-gated surface, add the check in head.js if the gate is route-level, or in the page's mounted/created if it's view-level. Don't scatter the same comparison across multiple components.

Impersonation

Impersonation is "admin signed in as a user." It is not a role per se — it's a flag (impersonationLogin cookie = true) that suppresses several side effects:

Side effect Suppressed when impersonating?
/updateProfile calls (last_login update) Yes (middleware/redirect.js → getUTZ)
Owner payment redirect Yes (middleware/head.js)
Welcome modal Verify with the team
impersonredirect post-login flow Triggers a one-shot redirect to /<accountId>/contentplanner

The cookie is set by the impersonation entry point (which verify with the team — the entry point is likely on an admin sub-domain or an internal tool). Frontend code should treat impersonation as read-only-ish: assume any "track this action" or "update this user's profile" call has been silently skipped.

When testing impersonation locally, check app.$cookies.get('impersonationLogin') === 'true' (the cookie is a string, not a boolean — comparisons in code are mostly correct but check whichever site you copy from).

Invitation flow

A user joining an existing account via invite goes through:

  1. Invite-email link arrives at /AcceptOrRejectInvite?token=....
  2. Frontend reads invite info from /getinvtokeninfo.
  3. On accept: /updateUserInvite, then /auth/AcceptOrRejectInvite (or similar — verify with the team for the exact endpoints).
  4. The new user lands with isInvited === 1 set on accountdetails.
  5. head.js skips the owner payment-required redirect because isInvited is truthy.

Invitation-management endpoints exist for owners: /getAllcreateInviteUserCorp, /addOrUpdateUserInvitationCorp, /UpdateAccountMembersactive, /DeleteinactiveUser, /DeleteInviteUsercorp.

Adding a new gated feature: checklist

  1. What's the gate? A route, a UI element, or a backend action? Route-level → add to head.js (and consider wiring head.js into the global chain). UI-level → conditionally render. Backend → ensure the API enforces too.
  2. What field do you gate on? Pick the existing field that fits (roleType, account_role, isInvited, noId). Don't introduce a new field without a backend change.
  3. Document the value here. Update the "Known values" table when you discover or define a new value.
  4. Trust the backend, not the frontend. Frontend gating is for UX, not security. Anything truly sensitive must also be enforced by the backend — never gate solely on a roleType check in a Vue component.
  5. Verify impersonation behaviour. If your gate has tracking or write side effects, decide explicitly whether impersonation should suppress them; mirror the pattern in redirect.js / head.js.

Open questions for the team

These are concrete unknowns from this audit:

  • What is the canonical mapping for all roleType values? Specifically, what is roleType === 11?
  • Is head.js intentionally unwired, or is wiring it the right next step?
  • Does noId strictly mean "BNI partner," or is it a more general cohort flag?
  • Are there account_role values besides 3 that have meaning?
  • What is the exact entry point that sets impersonationLogin cookie?