Fix nurse onboarding redirects and specialty Documents 404s.
Deploy Ladill Care / deploy (push) Successful in 36s
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:
@@ -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');
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user