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.jsis not in the global middleware chain innuxt.config.jsand 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 === 3block. A user with this role attempting to visit/my_library,/my_library#content,/dashboard/roi, or/dashboard/overviewis 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_idis 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/paymentof 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.accountIdto construct the post-login redirect target. - Reads
impersonationLoginto suppressupdateProfileand 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— usesroleTypeto 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 byOnboarding_type_id, which is itself a function ofnoId/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:
- Invite-email link arrives at
/AcceptOrRejectInvite?token=.... - Frontend reads invite info from
/getinvtokeninfo. - On accept:
/updateUserInvite, then/auth/AcceptOrRejectInvite(or similar — verify with the team for the exact endpoints). - The new user lands with
isInvited === 1set onaccountdetails. head.jsskips the owner payment-required redirect becauseisInvitedis truthy.
Invitation-management endpoints exist for owners: /getAllcreateInviteUserCorp, /addOrUpdateUserInvitationCorp, /UpdateAccountMembersactive, /DeleteinactiveUser, /DeleteInviteUsercorp.
Adding a new gated feature: checklist¶
- What's the gate? A route, a UI element, or a backend action? Route-level → add to
head.js(and consider wiringhead.jsinto the global chain). UI-level → conditionally render. Backend → ensure the API enforces too. - 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. - Document the value here. Update the "Known values" table when you discover or define a new value.
- 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
roleTypecheck in a Vue component. - 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
roleTypevalues? Specifically, what isroleType === 11? - Is
head.jsintentionally unwired, or is wiring it the right next step? - Does
noIdstrictly mean "BNI partner," or is it a more general cohort flag? - Are there
account_rolevalues besides3that have meaning? - What is the exact entry point that sets
impersonationLogincookie?