From 7b077905e213904613f76509edbc7c9d159ca6c1 Mon Sep 17 00:00:00 2001 From: mrhid6 Date: Sun, 26 Jul 2026 16:42:47 +0100 Subject: [PATCH] feat(web): hq-sourced members are read-only here MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .gitea/workflows/server-deploy.yml | 1 + web/Dockerfile | 6 ++++ web/app/(app)/settings/instance/page.tsx | 45 +++++++++++++++++------- web/lib/api.ts | 5 ++- 4 files changed, 44 insertions(+), 13 deletions(-) diff --git a/.gitea/workflows/server-deploy.yml b/.gitea/workflows/server-deploy.yml index 69ade1c..90d9b7e 100644 --- a/.gitea/workflows/server-deploy.yml +++ b/.gitea/workflows/server-deploy.yml @@ -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" diff --git a/web/Dockerfile b/web/Dockerfile index ab71d66..1fa8358 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -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 diff --git a/web/app/(app)/settings/instance/page.tsx b/web/app/(app)/settings/instance/page.tsx index 53adfd3..6db7975 100644 --- a/web/app/(app)/settings/instance/page.tsx +++ b/web/app/(app)/settings/instance/page.tsx @@ -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 ( @@ -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 ( @@ -138,22 +142,39 @@ function MembersCard() { )} - {u.auth_source === "oidc" ? "SSO" : "Password"} + + {u.auth_source === "oidc" ? "SSO" : u.auth_source === "hq" ? "Vantage HQ" : "Password"} + {u.last_login ? new Date(u.last_login).toLocaleString() : "Never"} - {!locked && ( - + {managedByHQ ? ( + hqUrl ? ( + + Managed in Vantage HQ + + ) : ( + Managed in Vantage HQ + ) + ) : ( + !locked && ( + + ) )} diff --git a/web/lib/api.ts b/web/lib/api.ts index 929adcb..9ed0b0e 100644 --- a/web/lib/api.ts +++ b/web/lib/api.ts @@ -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; }