Fix nurse onboarding redirects and specialty Documents 404s.
Deploy Ladill Care / deploy (push) Successful in 36s

Resolve staff memberships by invite/demo email as well as public_id so nurses stay on their employer org; block staff from owner onboarding; keep Documents viewable with module access while upload stays manage-gated.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-07-18 19:09:06 +00:00
co-authored by Cursor
parent 656f402e8f
commit c3219a1bf1
7 changed files with 524 additions and 14 deletions
@@ -26,6 +26,10 @@ class OnboardingController extends Controller
return redirect()->route('care.dashboard');
}
if (! $this->organizations->canCompleteOnboarding($request->user())) {
abort(403, 'Your facility setup is incomplete. Ask an administrator to finish onboarding.');
}
return view('care.onboarding.show', [
'user' => $request->user(),
'timezones' => timezone_identifiers_list(),
@@ -41,6 +45,10 @@ class OnboardingController extends Controller
return redirect()->route('care.dashboard');
}
if (! $this->organizations->canCompleteOnboarding($request->user())) {
abort(403, 'Your facility setup is incomplete. Ask an administrator to finish onboarding.');
}
$categoryKeys = array_keys($this->workflows->categories());
$templateKeys = array_keys($this->workflows->templates());
@@ -347,6 +347,7 @@ class SpecialtyModuleController extends Controller
$organization = $this->organization($request);
$member = $this->member($request);
// Viewing docs is workspace GET (memberCanAccess / memberCanView). Upload needs manage.
abort_unless($modules->memberCanAccess($organization, $member, $module), 403);
abort_unless($modules->memberCanManage($organization, $member, $module), 403);
abort_unless($visit->organization_id === $organization->id, 404);
@@ -25,6 +25,11 @@ class EnsureOrganizationSetup
}
if (! $this->organizations->isOnboarded($user)) {
// Staff already attached to a tenant must not see owner onboarding.
if ($this->organizations->membershipFor($user)) {
abort(403, 'Your facility setup is incomplete. Ask an administrator to finish onboarding.');
}
return redirect()->route('care.onboarding.show');
}
+45 -8
View File
@@ -670,17 +670,54 @@ class DemoTenantSeeder
$branch = $branches[$index % count($branches)];
}
Member::query()->updateOrCreate(
[
'organization_id' => $organization->id,
'user_ref' => strtolower($staff['email']),
],
[
$email = strtolower((string) $staff['email']);
// Prefer remapped public_id when the staff user already exists locally,
// so reseeds do not orphan SSO sessions keyed by public_id.
$publicId = User::query()
->whereRaw('LOWER(email) = ?', [$email])
->value('public_id');
$userRef = is_string($publicId) && $publicId !== '' ? $publicId : $email;
$existing = Member::query()
->where('organization_id', $organization->id)
->where(function ($query) use ($email, $publicId) {
$query->where('user_ref', $email);
if (is_string($publicId) && $publicId !== '') {
$query->orWhere('user_ref', $publicId);
}
})
->orderByRaw('CASE WHEN user_ref = ? THEN 0 ELSE 1 END', [$userRef])
->first();
if ($existing) {
$existing->forceFill([
'user_ref' => $userRef,
'owner_ref' => $ownerRef,
'role' => $role,
'branch_id' => $role === 'hospital_admin' ? null : $branch?->id,
],
);
])->save();
Member::query()
->where('organization_id', $organization->id)
->where('id', '!=', $existing->id)
->where(function ($query) use ($email, $publicId) {
$query->where('user_ref', $email);
if (is_string($publicId) && $publicId !== '') {
$query->orWhere('user_ref', $publicId);
}
})
->delete();
continue;
}
Member::query()->create([
'organization_id' => $organization->id,
'user_ref' => $userRef,
'owner_ref' => $ownerRef,
'role' => $role,
'branch_id' => $role === 'hospital_admin' ? null : $branch?->id,
]);
}
}
+80 -5
View File
@@ -17,9 +17,7 @@ class OrganizationResolver
{
public function resolveForUser(User $user): ?Organization
{
$ref = $user->ownerRef();
$member = Member::where('user_ref', $ref)->first();
$member = $this->membershipFor($user);
if ($member) {
$organization = Organization::query()->find($member->organization_id);
if ($organization) {
@@ -27,7 +25,7 @@ class OrganizationResolver
}
}
return Organization::owned($ref)->first();
return Organization::owned($user->ownerRef())->first();
}
public function forUser(User $user): Organization
@@ -44,6 +42,40 @@ class OrganizationResolver
&& (bool) data_get($organization->settings, 'onboarded', false);
}
/**
* Any Care membership for this user (public_id or invite/demo email key).
* Remaps email-keyed rows to public_id when found so staff stay linked after SSO.
*/
public function membershipFor(User $user): ?Member
{
$member = Member::query()->where('user_ref', $user->ownerRef())->first();
if ($member) {
return $member;
}
$email = strtolower(trim((string) $user->email));
if ($email === '' || $email === $user->ownerRef()) {
return null;
}
$member = Member::query()->where('user_ref', $email)->first();
return $member ? $this->remapMemberUserRef($member, $user) : null;
}
/**
* True when the user may run owner onboarding (create a facility).
* Staff already attached to a tenant must not create a second org.
*/
public function canCompleteOnboarding(User $user): bool
{
if ($this->membershipFor($user)) {
return false;
}
return ! $this->isOnboarded($user);
}
public function memberFor(User $user, ?Organization $organization = null): ?Member
{
$organization ??= $this->resolveForUser($user);
@@ -51,9 +83,52 @@ class OrganizationResolver
return null;
}
return Member::where('organization_id', $organization->id)
$member = Member::query()
->where('organization_id', $organization->id)
->where('user_ref', $user->ownerRef())
->first();
if ($member) {
return $member;
}
$email = strtolower(trim((string) $user->email));
if ($email === '') {
return null;
}
$member = Member::query()
->where('organization_id', $organization->id)
->where('user_ref', $email)
->first();
return $member ? $this->remapMemberUserRef($member, $user) : null;
}
/**
* Promote invite/demo email user_ref keys to the stable OIDC public_id.
*/
protected function remapMemberUserRef(Member $member, User $user): Member
{
$ref = $user->ownerRef();
if ($member->user_ref === $ref) {
return $member;
}
$conflict = Member::query()
->where('organization_id', $member->organization_id)
->where('user_ref', $ref)
->where('id', '!=', $member->id)
->first();
if ($conflict) {
$member->delete();
return $conflict;
}
$member->forceFill(['user_ref' => $ref])->save();
return $member->fresh() ?? $member;
}
public function ensureOwnerMember(User $user, Organization $organization): Member