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:
@@ -33,6 +33,7 @@ jobs:
|
||||
IMAGE="${{ vars.DOCKER_HOST }}/${{ github.repository_owner }}/vantage/web:latest"
|
||||
docker build \
|
||||
--build-arg NEXT_PUBLIC_API_URL="${{ vars.API_URL }}" \
|
||||
--build-arg NEXT_PUBLIC_HQ_URL="${{ vars.HQ_URL }}" \
|
||||
-t "$IMAGE" \
|
||||
-f web/Dockerfile web/
|
||||
docker push "$IMAGE"
|
||||
|
||||
@@ -17,6 +17,12 @@ COPY . .
|
||||
ARG NEXT_PUBLIC_API_URL=http://localhost:8080
|
||||
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
|
||||
|
||||
# Empty default on purpose: a self-hosted install has no HQ portal, and the
|
||||
# "Managed in Vantage HQ" label falls back to plain text rather than linking
|
||||
# somewhere that does not serve them.
|
||||
ARG NEXT_PUBLIC_HQ_URL=
|
||||
ENV NEXT_PUBLIC_HQ_URL=$NEXT_PUBLIC_HQ_URL
|
||||
|
||||
RUN npm run build
|
||||
|
||||
# Runtime stage
|
||||
|
||||
@@ -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>
|
||||
|
||||
+4
-1
@@ -335,7 +335,10 @@ export interface InstanceUser {
|
||||
instance_id: string;
|
||||
email: string;
|
||||
role: Role;
|
||||
auth_source: "local" | "oidc";
|
||||
// "hq" means the row was projected from a Vantage HQ account. Its role,
|
||||
// password and existence belong to HQ; this instance refuses to change them.
|
||||
auth_source: "local" | "oidc" | "hq";
|
||||
hq_user_id?: string;
|
||||
created_at: string;
|
||||
last_login?: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user