feat(web): hq-sourced members are read-only here
Server Deploy / deploy (push) Successful in 2m56s

The lock is a courtesy — the API answers 409 either way. NEXT_PUBLIC_HQ_URL
defaults empty so a self-hosted install shows a plain label rather than a
link to a portal that does not serve them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mrhid6
2026-07-26 16:42:47 +01:00
co-authored by Claude Opus 5
parent 2d12669f9b
commit 7b077905e2
4 changed files with 44 additions and 13 deletions
+33 -12
View File
@@ -69,6 +69,7 @@ function MembersCard() {
const isOwner = user?.role === "owner";
const assignableRoles = isOwner ? ROLES : ROLES.filter((r) => r !== "owner");
const hqUrl = process.env.NEXT_PUBLIC_HQ_URL ?? "";
return (
<Card>
@@ -113,7 +114,10 @@ function MembersCard() {
{users.map((u: InstanceUser) => {
const isSelf = u.user_id === user?.user_id;
const locked = isSelf || (u.role === "owner" && !isOwner);
const managedByHQ = u.auth_source === "hq";
// Locked here is a courtesy: the API returns 409 for an hq-sourced
// role change or deletion whether or not this select is rendered.
const locked = isSelf || managedByHQ || (u.role === "owner" && !isOwner);
return (
<Tr key={u.user_id}>
<Td>
@@ -138,22 +142,39 @@ function MembersCard() {
)}
</Td>
<Td>
<Badge variant="neutral">{u.auth_source === "oidc" ? "SSO" : "Password"}</Badge>
<Badge variant="neutral">
{u.auth_source === "oidc" ? "SSO" : u.auth_source === "hq" ? "Vantage HQ" : "Password"}
</Badge>
</Td>
<Td className="text-text-secondary">
{u.last_login ? new Date(u.last_login).toLocaleString() : "Never"}
</Td>
<Td className="text-right">
{!locked && (
<Button
variant="ghost"
size="sm"
onClick={() => {
if (confirm(`Remove ${u.email} from this instance?`)) removeUser(u.user_id);
}}
>
Remove
</Button>
{managedByHQ ? (
hqUrl ? (
<a
href={hqUrl}
target="_blank"
rel="noreferrer"
className="text-xs text-text-secondary underline"
>
Managed in Vantage HQ
</a>
) : (
<span className="text-xs text-text-tertiary">Managed in Vantage HQ</span>
)
) : (
!locked && (
<Button
variant="ghost"
size="sm"
onClick={() => {
if (confirm(`Remove ${u.email} from this instance?`)) removeUser(u.user_id);
}}
>
Remove
</Button>
)
)}
</Td>
</Tr>