Permissions System
|
Permissions are calculated on the client, from the |
Pipeline
userLoaded / userUpdated / userStatusUpdated
→ userPermissionsRequested
→ AuthEffects.loadedUserPermissions$
PermissionsService.getUserPermissions(user)
getDefaultPermissions() always granted
+ calculatePermissionsFromUserState() derived from User.status
+ getDevelopPermissions() non-production only
→ de-duplicated via Set
→ userPermissionsLoaded({ permissions })
→ permissionsReducer stores hierarchical Permission nodes
→ GlobalSubscriptionsService.loadRolesWithPermissions()
flushPermissions() then addPermission() for every node,
prefixing children with the parent name
→ NgxPermissionsService now answers *ngxPermissionsOnly and NgxPermissionsGuard
getUserPermissions(user: User): PermissionsEnum[] {
const permissions = [...this.getDefaultPermissions(), ...this.calculatePermissionsFromUserState(user)];
const developPermissions = this.getDevelopPermissions();
if (!this.environmentService.production) {
console.group('Please be aware those permissions are available only on dev environments');
console.log(developPermissions.map(p => PermissionsEnum[p]));
console.groupEnd();
permissions.push(...developPermissions);
}
return Array.from(new Set(permissions));
}
Recalculation triggers
| Action | Effect |
|---|---|
|
Fired after |
|
|
|
|
|
Re-fetches |
Any code that changes a supplier’s eligibility — a completed withdrawal, a signed
contract, an activation — dispatches userStatusUpdateRequested() so permissions
catch up.
Default permissions
Granted to every authenticated supplier, unconditionally.
| Permission | Grants |
|---|---|
|
The marketplace management page. |
|
The dashboard. |
|
Beta feature flag. |
|
Transfer-information settings screen. |
|
Long Term Contract features. |
|
Password change in security settings. |
|
2FA configuration. |
|
Shopify connector monitoring and reconnect prompts. |
|
Amazon connector monitoring and reconnect prompts. |
|
Dark-mode toggle. |
|
|
Development-only permissions
getDevelopPermissions() returns [NEW_EGG_PROVIDER], added only when
environment.production is false, and logged to the console as a reminder.
|
Anything gated on |
Advance Line permissions
calculateAdvanceLinePermissionsFromUser(user).
|
The whole block short-circuits when there is no Advance Line, or when
|
Three intermediate predicates:
const isAdvanceLineExpired = new Date() > new Date(advanceLine.expirationDate);
const isAdvanceLineActive = advanceLine.status === AdvanceLineStatusEnum.Active;
const isAdvanceLineRejected = advanceLine.uwStatus === AdvanceLineUWStatusEnum.Rejected;
| Permission | Condition |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Instant Advance permissions
calculateInstantAdvancePermissionsFromUser(user).
const hasValidActiveAdvance =
status.activeInstantAdvance &&
(!status.advanceLine || status.advanceLine?.type === AdvanceLineModeEnum.Legacy);
const hasValidPendingAdvance =
status.advanceLine?.type === AdvanceLineModeEnum.Legacy &&
status.advanceLine?.status === AdvanceLineStatusEnum.Pending;
| Permission | Condition |
|---|---|
|
|
|
|
|
|
This is the mutual exclusion between the two advance products: a supplier on a modern Advance Line never sees the Instant Advance widget.
Remaining state-based permissions
calculatePermissionsFromUserState(user).
| Permission | Condition |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
A |
Seller Card eligibility
private isSellerCardAvailable(user: User): boolean {
const hasPaymentHistory = user?.status?.hasPaymentHistory;
const pcardAvailable = user?.pcardAvailable;
const isFromUS = this.isFromUS(user);
return hasPaymentHistory && (pcardAvailable || isFromUS) && !this.isUserDuringOnboarding(user);
}
private isFromUS(user: User): boolean {
const country = user?._businessInformation?.address?.country;
const residentialCountry = user?._personalInformation?.residentialAddress?.country;
return [country, residentialCountry].every(code => code === CountryCode.US);
}
|
|
Shared predicates
private isUserPending = (s: SupplierStatusType): boolean =>
s === SupplierStatusType.PendingActivation || s === SupplierStatusType.Pending;
private isUserPendingAndExpedited = (user: User): boolean =>
this.isUserPending(user.status.supplierStatus) && user.schema === OnboardingSchemaEnum.Expedited;
private isUserDuringOnboarding = (user: User): boolean =>
user.dashboardStatus?.status === SupplierDashboardStatusType.Onboarding;
The permission enum
src/app/core/pyb/enums/permissions.enum.ts — a numeric enum, so values are
positional indices. Reordering members changes every persisted numeric value;
always append.
export enum PermissionsEnum {
ADVANCE_LINE,
ADVANCE_LINE_EXPIRED,
ADVANCE_LINE_PENDING_ACTIVATION,
AMAZON_API_CONNECTION,
BETA,
CHANGE_PASSWORD,
DAILY_SUMMARY,
DASHBOARD,
GOD_MODE,
INSTANT_ADVANCE_REPORT,
INSTANT_ADVANCE_REPORT_WIDGET,
INSTANT_ADVANCE_REPORT_MVP,
VIEW_INSTANT_ADVANCE_REPORT_MVP_ELEMENT,
INSTANT_TRANSFER_METHOD,
LCT_HISTORY,
LONG_TERM_CONTRACT,
MANAGE_TRANSFER_INFORMATION,
PROD_MODE,
REFERRAL,
REPORTS,
SELLER_CARD,
SHOPIFY_API_CONNECTION,
TRANSFER_FUNDS,
TRANSFER_INFORMATION,
TWO_FACTOR_SETUP,
VIEW_ADVANCE_LINE_REPORT,
VIEW_ADVANCE_LINE_WIDGET,
VIEW_NOTIFICATIONS,
WITHDRAW_ADVANCE_LINE,
INSTANT_ADVANCE_PENDING,
DARK_MODE,
NEW_EGG_PROVIDER,
MY_MARKETPLACES,
ADVANCE_LINE_REJECTED,
VENDOR_PAY,
}
GOD_MODE and PROD_MODE are declared and present in the permission tree but
are never granted by PermissionsService.
The permission tree
src/app/core/pyb/services/permissions.const.ts turns the flat enum into a
hierarchy. Children are addressed with a dotted, parent-prefixed name.
const permissions = [
{ id: PermissionsEnum.DASHBOARD },
{ id: PermissionsEnum.SELLER_CARD },
{
id: PermissionsEnum.REPORTS,
children: [
{ id: PermissionsEnum.INSTANT_ADVANCE_REPORT },
{ id: PermissionsEnum.INSTANT_ADVANCE_REPORT_MVP },
{ id: PermissionsEnum.VIEW_INSTANT_ADVANCE_REPORT_MVP_ELEMENT },
{ id: PermissionsEnum.DAILY_SUMMARY },
],
},
{ id: PermissionsEnum.TRANSFER_FUNDS },
{ id: PermissionsEnum.GOD_MODE },
{
id: PermissionsEnum.ADVANCE_LINE,
children: [
{ id: PermissionsEnum.ADVANCE_LINE_EXPIRED },
{ id: PermissionsEnum.ADVANCE_LINE_REJECTED },
{ id: PermissionsEnum.VIEW_ADVANCE_LINE_WIDGET },
{ id: PermissionsEnum.VIEW_ADVANCE_LINE_REPORT },
{ id: PermissionsEnum.WITHDRAW_ADVANCE_LINE },
{ id: PermissionsEnum.ADVANCE_LINE_PENDING_ACTIVATION },
],
},
// …flat entries for the remaining permissions
];
The default export is an IIFE that walks the tree and builds Permission nodes
with name, level, parentId and children.
Resulting names
| Enum member | ngx-permissions name |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Guards and template directives must use the full dotted name. This is why
|
|
|
Consuming permissions
In templates
<div *ngxPermissionsOnly="['SELLER_CARD']">
<pyb-seller-cards-widget></pyb-seller-cards-widget>
</div>
<div *ngxPermissionsExcept="['ADVANCE_LINE.ADVANCE_LINE_EXPIRED']">
<pyb-advance-line-widget></pyb-advance-line-widget>
</div>
In routes
{
path: 'daily-summary-report',
canActivate: [NgxPermissionsGuard],
data: { permissions: { only: ['REPORTS.DAILY_SUMMARY'] } },
}
From code
this.store.select(currentUserPermissions); // Permission[] from the store
this.ngxPermissionsService.hasPermission('SELLER_CARD'); // Promise<boolean>
this.ngxPermissionsService.removePermission('INSTANT_ADVANCE_PENDING');
Business flows revoke permissions optimistically after a successful action so the
UI updates before userStatusUpdateRequested() round-trips.
Adding a permission
-
Append the member to
PermissionsEnum— never insert in the middle. -
Add it to the tree in
permissions.const.ts, nested if it belongs under a parent. -
Grant it from
getDefaultPermissions()orcalculatePermissionsFromUserState(). -
Use the full dotted name in guards, templates and
MenuConfig. -
Extend
permissions.service.spec.tswith the new condition.