diff --git a/app/Http/Controllers/Care/OnboardingController.php b/app/Http/Controllers/Care/OnboardingController.php index 146190b..28f962a 100644 --- a/app/Http/Controllers/Care/OnboardingController.php +++ b/app/Http/Controllers/Care/OnboardingController.php @@ -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()); diff --git a/app/Http/Controllers/Care/SpecialtyModuleController.php b/app/Http/Controllers/Care/SpecialtyModuleController.php index 092ddba..81fee9d 100644 --- a/app/Http/Controllers/Care/SpecialtyModuleController.php +++ b/app/Http/Controllers/Care/SpecialtyModuleController.php @@ -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); diff --git a/app/Http/Middleware/EnsureOrganizationSetup.php b/app/Http/Middleware/EnsureOrganizationSetup.php index 7ede739..c53dac8 100644 --- a/app/Http/Middleware/EnsureOrganizationSetup.php +++ b/app/Http/Middleware/EnsureOrganizationSetup.php @@ -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'); } diff --git a/app/Services/Care/DemoTenantSeeder.php b/app/Services/Care/DemoTenantSeeder.php index 6c6208a..2e752e1 100644 --- a/app/Services/Care/DemoTenantSeeder.php +++ b/app/Services/Care/DemoTenantSeeder.php @@ -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, + ]); } } diff --git a/app/Services/Care/OrganizationResolver.php b/app/Services/Care/OrganizationResolver.php index 4cf8bc3..13ac5b6 100644 --- a/app/Services/Care/OrganizationResolver.php +++ b/app/Services/Care/OrganizationResolver.php @@ -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 diff --git a/resources/views/care/specialty/sections/workspace.blade.php b/resources/views/care/specialty/sections/workspace.blade.php index 09ba0ae..376ba97 100644 --- a/resources/views/care/specialty/sections/workspace.blade.php +++ b/resources/views/care/specialty/sections/workspace.blade.php @@ -88,7 +88,7 @@
Specialty documents attach to the patient chart for this episode.
- @if ($canManageClinical ?? $canManageSpecialty ?? true) + @if ($canManageClinical ?? $canManageSpecialty ?? false)