@cfxlabsinc/b2b-services
    Preparing search index...

    Module @cfxlabsinc/dashboard-auth-services

    dashboard-auth-services

    The customer and bank dashboards' auth services. Cognito authenticates a session and nothing else: users, memberships, roles, invites and activity are rows these services own. At the root: cfxStaff.ts, the CFX-staff email rule these services and both dashboards share; DashboardSsoSignInService, which tells the login page which tenant IdP an email belongs to; DashboardSsoUserAdminService, an operator's repair of a user's Cognito sign-in links after an SSO connection is deleted; and DashboardInviteMailer, the type of the invite-email sender the invite services take. ./clients/cognito holds CognitoAuthClient and CognitoAdminClient. A workspace is named by b2b.customer.name or b2b.bank.name, which CFX admins change.

    ./customer holds the customer dashboard's services over b2b tables: customer_dashboard_user, customer_dashboard_membership (one membership per workspace and user), customer_dashboard_user_activity, plus customer_dashboard_invite, customer_dashboard_workspace and the RBAC catalog. Each takes { db }; the invite services also take an inviteMailer. Customer-scoped methods name the caller as { dashboardUserId, customerId }: the user's external id (cdu_…; bank bdu_…) and the customer's id. A user's cognitoSub is their login, null until linked, and never their key. Nothing branches on which dashboard calls; the bank dashboard gets its own copies. See docs/proposals/SHARED_DASHBOARD_AUTH.md.

    • A workspace is a row: b2b.customer_dashboard_workspace or bank.bank_dashboard_workspace, one per customer or bank, reached through the tenant's workspace_id (DASHBOARD_WORKSPACE_TABLES.md). It holds the role set the dashboard assigns from, which *RoleSetAdminService.assign switches by locking and writing that row, and for a bank its dashboard domain, theme and enabled flag. Memberships, invites, SSO connections and activity name only their workspace_id, and a user's last-used workspace is last_workspace_id: a query that starts from a customer or bank reaches them through the tenant's workspace_id, and one that reports a tenant joins it back the same way. Each table's one unique index is on workspace_id (one membership per workspace and user, one live PENDING invite per workspace and address, one live SSO connection per workspace). Services still address a tenant by customer or bank id; workspaceId on their results is the workspace's external id (bws_… for a bank).
    • BankDashboardMembershipService.searchOwn returns each membership's workspaceId, dashboardDomain, dashboardEnabled and theme, and can be limited to banks whose dashboard is enabled. That is what bank-dashboard picks its one active bank from: the host is no part of it.
    • CustomerDashboardMembershipService and CustomerDashboardInviteService authorize inside the query, joining the caller's ACTIVE membership to their role's grants; a member is addressed by customer and user id.
    • CustomerDashboardMembershipService.searchOwn (bank: BankDashboardMembershipService.searchOwn) is the dashboard's per-request principal: the caller's own live memberships, with each one's grants and their user row. A DISABLED membership is listed with no grants, as is an internal role held by anyone but confirmed CFX staff. It needs no permission.
    • Postgres alone decides access. A role or status change, a removal and the caller's own rename each write only rows: no auth vendor is told.
    • A member is disabled and re-enabled through *MembershipService.update with status (ACTIVE or DISABLED), which needs team/role:w and a member within the caller's reach who isn't the caller. A DISABLED member keeps their role. The change is recorded as user.workspaceStatusChanged in the same transaction. The last ACTIVE holder of the top role cannot be disabled (LAST_OWNER).
    • An invite is a row (inv_…) that is written, then emailed, and accepted by a sign-in that proves its address. *InviteService.create and *InviteAdminService.create take a dashboardOrigin and send the email through the inviteMailer they were built with. Every caller builds DashboardInviteEmailSender from @cfxlabsinc/notification-services/dashboard-invite, which sends through SES from the address its caller passes (NOTIFICATION_FROM_EMAIL in the three dashboards, DASHBOARD_INVITE_FROM_EMAIL in temporal-worker) with a link to <dashboardOrigin>/signup?inviteId=<inv_…>&email=<address>. A send that fails withdraws the row, so none stays PENDING for an address nobody emailed. The member-facing *InviteService.create refuses an address that is already a member (WORKSPACE_INVITE_REJECTED). CustomerDashboardInviteAdminService.create is also onboarding's owner invite. inviteAcceptableSince and inviteExpiresAt are exported from ./customer and ./bank.
    • CustomerDashboardMembershipAdminService and BankDashboardMembershipAdminService are for callers with no dashboard user: CFX operators re-roling, disabling, re-enabling and removing members, and a Cognito sign-in accepting its invites. An operator's update keeps the LAST_OWNER rule and records nothing in the workspace's feed: admin-dashboard keeps its own activity log. acceptPendingInvites takes the proven address's PENDING invites from the last 14 days (INVITE_ACCEPTANCE_DAYS), decides each membership by the pure rules in src/membershipSync.ts, and records each join as activity in the same transaction.
    • CustomerDashboardUserActivityService records and reads the activity feed. CustomerDashboardUserService (bank: BankDashboardUserService) reads users by id, including those who have left, and is the only writer of their rows: createFromCognito for an invitee's first sign-in, linkCognito, update (name, profile picture and phone number), and markSeen.
    • CustomerDashboardPermissionAdminService reads the whole permission catalog.
    • CustomerNotificationService.getRecipients resolves an event's email recipients for email-notification-handler, reading the matched members and their overlays in one query. CustomerDashboardUserNotificationPreferencesService reads and updates a member's own email toggles, and its update never creates a membership. The pure wantsEmail in src/customer/notificationPreferences.ts applies them.

    These services read no PropelAuth identifier. A customer's workspace key (workspaceId on these services) is its workspace row's external_id: the value the customer already had, or cws_… for one created since. A user is named only by their own cdu_… / bdu_… id: the PropelAuth user ids that historical JSONB held (transaction creators, terms acceptances, team activity rows, notification userIds) were rewritten to those ids.

    The PropelAuth columns are gone: 20261010040000_drop_legacy_auth_columns.sql dropped b2b.customer.propelauth_workspace_id and workspace_external_id with the trigger customer_workspace_external_id_sync, b2b.bank.propelauth_org_id, propelauth_id on both dashboard user tables and propelauth_invite_id on both invite tables (COGNITO_AUTH_MIGRATION.md § Phase 2). The other columns the workspace tables replaced (customer.dashboard_role_set_id, bank.dashboard_role_set_id, bank.dashboard_domain, bank.theme) are neither read nor written by these services: they are frozen at the values the workspace rows were filled from, and their drop is still deferred (DASHBOARD_WORKSPACE_TABLES.md § 3).

    admin
    admin/descriptions
    bank
    bank/descriptions
    billing-attention
    clients/cognito
    config
    customer
    customer/descriptions
    events-token
    identity
    organization
    person-name
    safe-redirect
    schedules
    staff-email
    workflows