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; }