refactor(web): rename Organisation to Instance

This commit is contained in:
2026-07-24 14:01:19 +01:00
parent 3f0f12b111
commit b70ccc97d4
8 changed files with 76 additions and 76 deletions
@@ -2,7 +2,7 @@
import { useEffect, useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { api, auth as authApi, type OrgUser, type Role } from "@/lib/api";
import { api, auth as authApi, type InstanceUser, type Role } from "@/lib/api";
import { useAuth } from "@/components/AuthProvider";
import { Badge, Button, Card, Modal, Table, Tbody, Td, Th, Thead, Tr } from "@/components/ui";
@@ -35,12 +35,12 @@ function MembersCard() {
const [password, setPassword] = useState("");
const [role, setRole] = useState<Role>("member");
const { data: users, isLoading, error } = useQuery({ queryKey: ["org-users"], queryFn: api.listOrgUsers });
const { data: users, isLoading, error } = useQuery({ queryKey: ["instance-users"], queryFn: api.listInstanceUsers });
const invalidate = () => queryClient.invalidateQueries({ queryKey: ["org-users"] });
const invalidate = () => queryClient.invalidateQueries({ queryKey: ["instance-users"] });
const { mutate: createUser, isPending: creating, error: createError } = useMutation({
mutationFn: () => api.createOrgUser({ email, password, role }),
mutationFn: () => api.createInstanceUser({ email, password, role }),
onSuccess: () => {
invalidate();
setAddOpen(false);
@@ -51,7 +51,7 @@ function MembersCard() {
});
const { mutate: changeRole, error: roleError } = useMutation({
mutationFn: ({ userId, next }: { userId: string; next: Role }) => api.updateOrgUserRole(userId, next),
mutationFn: ({ userId, next }: { userId: string; next: Role }) => api.updateInstanceUserRole(userId, next),
onSuccess: invalidate,
@@ -59,7 +59,7 @@ function MembersCard() {
});
const { mutate: removeUser, error: removeError } = useMutation({
mutationFn: (userId: string) => api.deleteOrgUser(userId),
mutationFn: (userId: string) => api.deleteInstanceUser(userId),
onSuccess: invalidate,
});
@@ -76,7 +76,7 @@ function MembersCard() {
<div>
<h2 className="text-base font-semibold text-text-primary">Members</h2>
<p className="mt-0.5 text-sm text-text-secondary">
People with access to this organization. Owners and admins can manage settings.
People with access to this instance. Owners and admins can manage settings.
</p>
</div>
<Button variant="primary" size="sm" onClick={() => setAddOpen(true)}>
@@ -110,7 +110,7 @@ function MembersCard() {
</Tr>
</Thead>
<Tbody>
{users.map((u: OrgUser) => {
{users.map((u: InstanceUser) => {
const isSelf = u.user_id === user?.user_id;
const locked = isSelf || (u.role === "owner" && !isOwner);
@@ -149,7 +149,7 @@ function MembersCard() {
variant="ghost"
size="sm"
onClick={() => {
if (confirm(`Remove ${u.email} from this organization?`)) removeUser(u.user_id);
if (confirm(`Remove ${u.email} from this instance?`)) removeUser(u.user_id);
}}
>
Remove
@@ -226,7 +226,7 @@ function MembersCard() {
function OIDCCard() {
const queryClient = useQueryClient();
const { data: cfg, isLoading } = useQuery({ queryKey: ["org-oidc"], queryFn: api.getOrgOIDC });
const { data: cfg, isLoading } = useQuery({ queryKey: ["instance-oidc"], queryFn: api.getInstanceOIDC });
const [issuer, setIssuer] = useState("");
const [clientId, setClientId] = useState("");
@@ -247,9 +247,9 @@ function OIDCCard() {
}, [cfg]);
const { mutate: save, isPending, error } = useMutation({
mutationFn: () => api.saveOrgOIDC({ issuer, client_id: clientId, client_secret: clientSecret, enabled }),
mutationFn: () => api.saveInstanceOIDC({ issuer, client_id: clientId, client_secret: clientSecret, enabled }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["org-oidc"] });
queryClient.invalidateQueries({ queryKey: ["instance-oidc"] });
setClientSecret("");
setSaved(true);
setTimeout(() => setSaved(false), 3000);
@@ -279,7 +279,7 @@ function OIDCCard() {
<div className="mb-4">
<h2 className="text-base font-semibold text-text-primary">Single Sign-On (OIDC)</h2>
<p className="mt-0.5 text-sm text-text-secondary">
Let members sign in with your identity provider. Users are provisioned into this organization on
Let members sign in with your identity provider. Users are provisioned into this instance on
first sign-in.
</p>
</div>
@@ -357,7 +357,7 @@ function OIDCCard() {
onChange={(e) => setEnabled(e.target.checked)}
className="h-4 w-4 rounded border-border bg-surface-2 accent-accent"
/>
Enable SSO sign-in for this organization
Enable SSO sign-in for this instance
</label>
{enabled && !secretSet && !clientSecret && (
@@ -383,8 +383,8 @@ function OIDCCard() {
);
}
export default function OrgSettingsPage() {
const { org, isAdmin } = useAuth();
export default function InstanceSettingsPage() {
const { instance, isAdmin } = useAuth();
if (!isAdmin) {
return (
@@ -392,7 +392,7 @@ export default function OrgSettingsPage() {
<Card className="max-w-lg">
<h1 className="text-base font-semibold text-text-primary">You don&apos;t have access</h1>
<p className="mt-1 text-sm text-text-secondary">
Organization settings are available to owners and admins only. Ask an administrator if you need
Instance settings are available to owners and admins only. Ask an administrator if you need
access.
</p>
</Card>
@@ -403,9 +403,9 @@ export default function OrgSettingsPage() {
return (
<div className="p-8">
<div className="mb-8">
<h1 className="text-2xl font-bold text-text-primary">Organization</h1>
<h1 className="text-2xl font-bold text-text-primary">Instance</h1>
<p className="mt-1 text-sm text-text-secondary">
{org ? `Manage members and sign-in for ${org.name}.` : "Manage members and sign-in."}
{instance ? `Manage members and sign-in for ${instance.name}.` : "Manage members and sign-in."}
</p>
</div>
+5 -5
View File
@@ -10,7 +10,7 @@ import { NetworkBackground } from "@/components/NetworkBackground";
export default function LoginPage() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [orgName, setOrgName] = useState("");
const [instanceName, setInstanceName] = useState("");
useEffect(() => {
(async () => {
@@ -20,7 +20,7 @@ export default function LoginPage() {
window.location.href = "/setup";
return;
}
if (s.org_name) setOrgName(s.org_name);
if (s.instance_name) setInstanceName(s.instance_name);
} catch {}
try {
await auth.me();
@@ -52,7 +52,7 @@ export default function LoginPage() {
<div className="mb-8 flex flex-col items-center gap-3">
<Logo className="h-11 w-auto text-text-primary" />
<h1 className="text-2xl font-semibold text-text-primary">Sign in to Vantage</h1>
<h4 className="font-semibold text-text-secondary">Organisation: {orgName}</h4>
<h4 className="font-semibold text-text-secondary">Instance: {instanceName}</h4>
</div>
<Card>
@@ -102,10 +102,10 @@ export default function LoginPage() {
<a href="/auth/oidc/start" className="block">
<Button type="button" variant="secondary" className="w-full justify-center">
Sign in with your organization&apos;s SSO
Sign in with your instance&apos;s SSO
</Button>
</a>
<p className="mt-3 text-center text-xs text-text-tertiary">SSO must be enabled for this organization by an administrator.</p>
<p className="mt-3 text-center text-xs text-text-tertiary">SSO must be enabled for this instance by an administrator.</p>
</Card>
</div>
</div>
+17 -17
View File
@@ -8,17 +8,17 @@ import { Button, Card } from "@/components/ui";
const MIN_PASSWORD_LENGTH = 8;
/**
* Org hosts are `<slug>.vantage.<rest>` and the apex is `vantage.<rest>` (see
* auth.hostSlug on the server). Build the new org's URL by prepending or
* Instance hosts are `<slug>.vantage.<rest>` and the apex is `vantage.<rest>` (see
* auth.hostSlug on the server). Build the new instance's URL by prepending or
* replacing the leftmost label. Hosts that don't match that shape (localhost,
* bare IPs) have no per-org subdomain, so stay put.
* bare IPs) have no per-instance subdomain, so stay put.
*
* Setup runs on the apex, and the session cookie it sets is scoped to that
* exact host by design org hosts must not share cookies. So the new owner is
* sent to the org host's *login* page to sign in there, which is what puts a
* session cookie on the host their org actually lives on.
* exact host by design instance hosts must not share cookies. So the new owner is
* sent to the instance host's *login* page to sign in there, which is what puts a
* session cookie on the host their instance actually lives on.
*/
function orgLoginUrlForSlug(slug: string): string {
function instanceLoginUrlForSlug(slug: string): string {
if (typeof window === "undefined") return "/login";
const { protocol, host } = window.location;
const [hostname, port] = host.split(":");
@@ -34,7 +34,7 @@ function orgLoginUrlForSlug(slug: string): string {
}
export default function SetupPage() {
const [orgName, setOrgName] = useState("");
const [instanceName, setInstanceName] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [confirm, setConfirm] = useState("");
@@ -54,9 +54,9 @@ export default function SetupPage() {
isPending,
error,
} = useMutation({
mutationFn: () => auth.bootstrap({ org_name: orgName, email, password }),
mutationFn: () => auth.bootstrap({ instance_name: instanceName, email, password }),
onSuccess: (res) => {
setCreated({ slug: res.slug, loginUrl: orgLoginUrlForSlug(res.slug) });
setCreated({ slug: res.slug, loginUrl: instanceLoginUrlForSlug(res.slug) });
},
});
@@ -83,13 +83,13 @@ export default function SetupPage() {
<div className="flex min-h-screen items-center justify-center bg-background p-4">
<div className="w-full max-w-md">
<div className="mb-8 text-center">
<h1 className="text-xl font-semibold text-text-primary">Organization created</h1>
<h1 className="text-xl font-semibold text-text-primary">Instance created</h1>
<p className="mt-1 text-sm text-text-secondary">Your owner account is ready. One more step to finish signing in.</p>
</div>
<Card>
<p className="text-sm text-text-secondary">
{created.slug} has its own address, and sign-in is kept separate per organization. Continue to your organization&apos;s sign-in page and log in with the email and password
{created.slug} has its own address, and sign-in is kept separate per instance. Continue to your instance&apos;s sign-in page and log in with the email and password
you just chose.
</p>
<code className="mt-3 block overflow-x-auto rounded bg-surface-2 px-2 py-1.5 font-mono text-xs text-text-primary">{created.loginUrl}</code>
@@ -109,17 +109,17 @@ export default function SetupPage() {
<div className="w-full max-w-md">
<div className="mb-8 text-center">
<h1 className="text-xl font-semibold text-text-primary">Welcome to Vantage</h1>
<p className="mt-1 text-sm text-text-secondary">Create your organization and its owner account to get started.</p>
<p className="mt-1 text-sm text-text-secondary">Create your instance and its owner account to get started.</p>
</div>
<Card>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="org" className="mb-1.5 block text-sm font-medium text-text-secondary">
Organization name
<label htmlFor="instance" className="mb-1.5 block text-sm font-medium text-text-secondary">
Instance name
</label>
<input id="org" type="text" required value={orgName} onChange={(e) => setOrgName(e.target.value)} className={inputClass} />
<p className="mt-1 text-xs text-text-tertiary">Used to derive your organization&apos;s subdomain.</p>
<input id="instance" type="text" required value={instanceName} onChange={(e) => setInstanceName(e.target.value)} className={inputClass} />
<p className="mt-1 text-xs text-text-tertiary">Used to derive your instance&apos;s subdomain.</p>
</div>
<div>
+8 -8
View File
@@ -1,18 +1,18 @@
"use client";
import { createContext, useContext, useEffect, useState, ReactNode } from "react";
import { auth, type Org, type Role, type SessionUser } from "@/lib/api";
import { auth, type Instance, type Role, type SessionUser } from "@/lib/api";
export type { Org, Role, SessionUser };
export type { Instance, Role, SessionUser };
interface AuthContextType {
user: SessionUser | null;
org: Org | null;
/** True for owner and admin the roles the /api/settings and /api/org routes require. */
instance: Instance | null;
/** True for owner and admin the roles the /api/settings and /api/instance routes require. */
isAdmin: boolean;
}
const AuthContext = createContext<AuthContextType>({ user: null, org: null, isAdmin: false });
const AuthContext = createContext<AuthContextType>({ user: null, instance: null, isAdmin: false });
export function useAuth() {
return useContext(AuthContext);
@@ -24,7 +24,7 @@ export function useAuth() {
*/
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<SessionUser | null>(null);
const [org, setOrg] = useState<Org | null>(null);
const [instance, setOrg] = useState<Instance | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
@@ -42,7 +42,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
const me = await auth.me();
if (cancelled) return;
setUser(me.user);
setOrg(me.org);
setOrg(me.instance);
setLoading(false);
} catch (err) {
if (cancelled) return;
@@ -91,5 +91,5 @@ export function AuthProvider({ children }: { children: ReactNode }) {
const isAdmin = user.role === "owner" || user.role === "admin";
return <AuthContext.Provider value={{ user, org, isAdmin }}>{children}</AuthContext.Provider>;
return <AuthContext.Provider value={{ user, instance, isAdmin }}>{children}</AuthContext.Provider>;
}
+4 -4
View File
@@ -108,7 +108,7 @@ function StepsIcon() {
);
}
function OrgIcon() {
function InstanceIcon() {
return (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path
@@ -128,13 +128,13 @@ const navItems: NavItem[] = [
{ href: "/workflows", label: "Workflows", icon: <WorkflowIcon /> },
{ href: "/steps", label: "Steps", icon: <StepsIcon /> },
{ href: "/audit", label: "Audit Log", icon: <AuditIcon /> },
{ href: "/settings/org", label: "Organization", icon: <OrgIcon />, adminOnly: true },
{ href: "/settings/instance", label: "Instance", icon: <InstanceIcon />, adminOnly: true },
{ href: "/settings", label: "Settings", icon: <SettingsIcon />, adminOnly: true },
];
export function Sidebar() {
const pathname = usePathname();
const { user, org, isAdmin } = useAuth();
const { user, instance, isAdmin } = useAuth();
const visibleItems = navItems.filter((item) => !item.adminOnly || isAdmin);
@@ -157,7 +157,7 @@ export function Sidebar() {
<Logo className="h-8 w-8 text-text-primary" />
<div className="min-w-0">
<span className="block text-base font-semibold leading-tight text-text-primary">Vantage</span>
{org && <span className="block truncate text-xs text-text-secondary">{org.name}</span>}
{instance && <span className="block truncate text-xs text-text-secondary">{instance.name}</span>}
</div>
</div>
+1 -1
View File
@@ -47,7 +47,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
{loading && (
<svg
className="animate-spin h-4 w-4"
xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.instance/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
+21 -21
View File
@@ -301,14 +301,14 @@ export type Role = "owner" | "admin" | "member";
/** The session as returned by GET /auth/me mirrors auth.Session on the server. */
export interface SessionUser {
user_id: string;
org_id: string;
instance_id: string;
role: Role;
email: string;
name: string;
}
export interface Org {
org_id: string;
export interface Instance {
instance_id: string;
name: string;
slug: string;
created_at: string;
@@ -316,22 +316,22 @@ export interface Org {
export interface MeResponse {
user: SessionUser;
org: Org | null;
instance: Instance | null;
}
export interface BootstrapStatus {
needs_setup: boolean;
org_name?: string;
instance_name?: string;
}
export interface BootstrapResponse {
org: Org;
instance: Instance;
slug: string;
}
export interface OrgUser {
export interface InstanceUser {
user_id: string;
org_id: string;
instance_id: string;
email: string;
role: Role;
auth_source: "local" | "oidc";
@@ -426,7 +426,7 @@ export const auth = {
return authRequest<BootstrapStatus>("/auth/bootstrap-status");
},
bootstrap(input: { org_name: string; email: string; password: string }): Promise<BootstrapResponse> {
bootstrap(input: { instance_name: string; email: string; password: string }): Promise<BootstrapResponse> {
return authRequest<BootstrapResponse>("/auth/bootstrap", {
method: "POST",
body: JSON.stringify(input),
@@ -456,31 +456,31 @@ export const auth = {
};
export const api = {
listOrgUsers(): Promise<OrgUser[]> {
return request<OrgUser[]>("/org/users");
listInstanceUsers(): Promise<InstanceUser[]> {
return request<InstanceUser[]>("/instance/users");
},
createOrgUser(input: OrgUserInput): Promise<OrgUser> {
return request<OrgUser>("/org/users", { method: "POST", body: JSON.stringify(input) });
createInstanceUser(input: OrgUserInput): Promise<InstanceUser> {
return request<InstanceUser>("/instance/users", { method: "POST", body: JSON.stringify(input) });
},
updateOrgUserRole(userId: string, role: Role): Promise<{ ok: boolean }> {
return request<{ ok: boolean }>(`/org/users/${userId}/role`, {
updateInstanceUserRole(userId: string, role: Role): Promise<{ ok: boolean }> {
return request<{ ok: boolean }>(`/instance/users/${userId}/role`, {
method: "PUT",
body: JSON.stringify({ role }),
});
},
deleteOrgUser(userId: string): Promise<void> {
return request<void>(`/org/users/${userId}`, { method: "DELETE" });
deleteInstanceUser(userId: string): Promise<void> {
return request<void>(`/instance/users/${userId}`, { method: "DELETE" });
},
getOrgOIDC(): Promise<OrgOIDCConfig> {
return request<OrgOIDCConfig>("/org/oidc");
getInstanceOIDC(): Promise<OrgOIDCConfig> {
return request<OrgOIDCConfig>("/instance/oidc");
},
saveOrgOIDC(input: OrgOIDCInput): Promise<{ saved: boolean }> {
return request<{ saved: boolean }>("/org/oidc", { method: "PUT", body: JSON.stringify(input) });
saveInstanceOIDC(input: OrgOIDCInput): Promise<{ saved: boolean }> {
return request<{ saved: boolean }>("/instance/oidc", { method: "PUT", body: JSON.stringify(input) });
},
listServers(): Promise<Server[]> {
File diff suppressed because one or more lines are too long