refactor(web): rename Organisation to Instance
This commit is contained in:
@@ -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'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>
|
||||
|
||||
@@ -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's SSO
|
||||
Sign in with your instance'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
@@ -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'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'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'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's subdomain.</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
@@ -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
Reference in New Issue
Block a user