feat(byra): gate the automatic cockpit landing to owner/admin (#1970)

* feat(byra): gate the automatic cockpit landing to owner/admin

Plain byra members now land like regular users; owner/admin keep the
cockpit landing at both decision sites (post-login /api/clients/landing
and the '/' bounce). The middleware zero-company steer stays ungated:
a member with zero companies has nowhere else to land. Cockpit access
itself is unchanged (nav + /clients remain membership-based).

Supersedes the 2026-08-05 all-members widening (DECISIONS.md).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(byra): keep byra members out of the first-run wizard on auto-landing

Skeptic finding: a member whose auto-resolved active company is
onboarding-incomplete (e.g. mid migration-reset, which repoints
active_company_id itself) fell through the new role gate into
/onboarding, a dead end for role member (WL-15 refuses client
creation). Byra members without a picked-company cookie now go to
/byra at the onboarding check, restoring the pre-gate shield.

Also pins the role column into the landing route's select assertion
so dropping it can't pass the mocked tests silently.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mattsson
2026-08-27 11:49:00 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent 23776337c8
commit b30c71086e
7 changed files with 193 additions and 14 deletions
+20
View File
@@ -2,6 +2,7 @@ import { describe, it, expect } from 'vitest'
import {
partitionCompaniesByHomeDomain,
isCompanyHomedOnHost,
isCockpitLandingRole,
resolveLandingPath,
type TeamBrandRef,
} from '../home-domain'
@@ -138,6 +139,25 @@ describe('isCompanyHomedOnHost', () => {
})
})
describe('isCockpitLandingRole', () => {
it('owner gets the automatic cockpit landing', () => {
expect(isCockpitLandingRole('owner')).toBe(true)
})
it('admin gets the automatic cockpit landing', () => {
expect(isCockpitLandingRole('admin')).toBe(true)
})
it('plain member lands like a regular user', () => {
expect(isCockpitLandingRole('member')).toBe(false)
})
it('unknown roles default to the regular landing (allowlist)', () => {
expect(isCockpitLandingRole('viewer')).toBe(false)
expect(isCockpitLandingRole('')).toBe(false)
})
})
describe('resolveLandingPath', () => {
it("byrå staff on their brand host land in the cockpit", () => {
expect(
+15 -1
View File
@@ -95,11 +95,25 @@ export function isCompanyHomedOnHost(opts: {
return opts.companyTeamId === opts.hostBrandTeamId
}
/**
* Whether a byrå team role gets the AUTOMATIC cockpit landing (2026-08-27:
* owner/admin only, superseding the 2026-08-05 all-members widening). Plain
* members land like regular users; they can still open the cockpit manually
* (nav visibility and access are membership-based, unchanged). Callers drop
* non-qualifying memberships BEFORE resolveLandingPath, which stays pure.
* Allowlist, not `role !== 'member'`, so any future role defaults to the
* regular landing.
*/
export function isCockpitLandingRole(role: string): boolean {
return role === 'owner' || role === 'admin'
}
/**
* Post-login landing (WL-14): byrå staff land in the cockpit on the byrå's
* home domain: the brand domain when the team has a brand, else the canonical
* domain (a byrå team without white label is a valid state, WL-01). Everyone
* else keeps today's '/' byte-identically.
* else keeps today's '/' byte-identically. Callers pass only memberships that
* pass isCockpitLandingRole.
*/
export function resolveLandingPath(opts: {
/** teams.id of the brand serving the current host, null on canonical. */
+4 -2
View File
@@ -499,8 +499,10 @@ async function updateSessionInner(
return supabaseResponse
}
// Byrå team members (any role: widened from owner/admin, founder call
// 2026-08-05) with zero client companies (a fresh byrå) home to the
// Byrå team members (any role: deliberately NOT gated by
// isCockpitLandingRole even after the 2026-08-27 owner/admin landing
// gate, because a plain member with zero companies has nowhere else to
// land) with zero client companies (a fresh byrå) home to the
// EMPTY cockpit, never to the company onboarding wizard: clients are
// created from the cockpit, and forcing the wizard here would make a
// byrå user create a personal company just to get in. Cockpit-shaped