Admin Users: show each user's org and move a user into yours
A viewer whose first login ran self-serve signup got an empty org of
their own (org_role owner), so they saw no incidents or calls, and the
earlier fix deliberately never moved a user who already had an org.
PATCH /admin/users/{uid} now moves a user when org_id is passed
explicitly (claims + org_members, audited with left_org_id; the old org
is not deleted). The user list returns org_id/org_role, and the admin
user panel shows the org and a 'Move to my organization' button when it
isn't yours.
Verified: c2-core pytest 496 passed; frontend tsc --noEmit clean.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
fa41b9a30c
commit
2b42e5ee9a
@@ -367,6 +367,26 @@ function UserDetailPanel({
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [showSessions, setShowSessions] = useState(false);
|
||||
const { orgId: myOrgId } = useAuth();
|
||||
const [moving, setMoving] = useState(false);
|
||||
|
||||
// A user outside the admin's org reads none of its incidents or calls —
|
||||
// e.g. a viewer whose first login self-provisioned an empty org.
|
||||
async function handleMoveToMyOrg() {
|
||||
if (!myOrgId) return;
|
||||
if (!confirm(`Move ${detail.email ?? "this user"} into your organization as a member? They'll need to sign out and back in.`)) return;
|
||||
setMoving(true);
|
||||
setError(null);
|
||||
try {
|
||||
const updated = await c2api.updateUser(user.uid, { org_id: myOrgId });
|
||||
onUpdated(updated);
|
||||
setDetail((d) => ({ ...d, ...updated }));
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : String(e));
|
||||
} finally {
|
||||
setMoving(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch full detail (sessions) lazily
|
||||
useEffect(() => {
|
||||
@@ -496,6 +516,25 @@ function UserDetailPanel({
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-800 pt-4 space-y-2 text-xs">
|
||||
<div className="flex justify-between items-center gap-3">
|
||||
<span className="text-gray-500">Organization</span>
|
||||
<span className={`font-mono truncate ${detail.org_id && detail.org_id === myOrgId ? "text-gray-300" : "text-yellow-400"}`}>
|
||||
{!detail.org_id
|
||||
? "None: sees no data"
|
||||
: detail.org_id === myOrgId
|
||||
? `Your org (${detail.org_role ?? "member"})`
|
||||
: `Other org ${detail.org_id.slice(0, 8)}… (${detail.org_role ?? "member"})`}
|
||||
</span>
|
||||
</div>
|
||||
{myOrgId && detail.org_id !== myOrgId && (
|
||||
<button
|
||||
onClick={handleMoveToMyOrg}
|
||||
disabled={moving}
|
||||
className="w-full bg-yellow-900/60 hover:bg-yellow-800/60 disabled:opacity-50 text-yellow-200 px-3 py-1.5 rounded-lg transition-colors"
|
||||
>
|
||||
{moving ? "Moving…" : "Move to my organization"}
|
||||
</button>
|
||||
)}
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">Status</span>
|
||||
<span className={detail.disabled ? "text-red-400" : "text-green-400"}>
|
||||
|
||||
Reference in New Issue
Block a user