Phase 5: platform feature-flags CRUD, impersonation, billing catalogue
Three new platform screens on top of the Phase 1-4 work. Feature flags (/feature-flags) — platform-wide flag registry. New route + lib/arcadia/feature-flags.ts, capability platform.feature_flags, nav under Automation. List/create/edit/delete with a per-row default toggle; pairs with the Phase-4 per-tenant override tab. Impersonation — "Impersonate" action on active users. Entirely client-side token swap in session.ts (beginImpersonation parks the operator's session + API token and swaps to the impersonation token; endImpersonation restores it), with a sticky "Viewing as <email> — Stop" banner in the shell driven by the JWT's impersonated_by claim. Stop is client-side because the impersonation token carries the target's roles and can't reach the admin-gated /stop endpoint; impersonation is stateless JWT so restoring the parked token is sufficient. Billing (/billing) — replaced the coming-soon stub with the real plan catalogue from GET /billing/plans (lib/arcadia/billing.ts). Per-tenant plan assignment stays on the tenant detail page; Entitlements + Apps remain honestly marked "Soon". Verified in-browser with real backend; typecheck adds zero errors (36→36). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -76,13 +76,15 @@ import {
|
||||
deleteUser,
|
||||
listUsers,
|
||||
setUserStatus,
|
||||
startImpersonation,
|
||||
updateUser,
|
||||
type User,
|
||||
type UserInput,
|
||||
type UserStatus,
|
||||
} from "~/lib/arcadia/users"
|
||||
import { pageTitle } from "~/lib/page-meta"
|
||||
import { useSession } from "~/lib/session"
|
||||
import { beginImpersonation, useSession } from "~/lib/session"
|
||||
import { useNavigate } from "react-router"
|
||||
import { useRegisterContext } from "@crema/aifirst-ui/context"
|
||||
import { UserDetailSheet } from "~/components/users/user-detail-sheet"
|
||||
|
||||
@@ -246,6 +248,21 @@ function UsersPanel({
|
||||
}) {
|
||||
const arcadia = useArcadiaClient()
|
||||
const toast = useToast()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const impersonate = async (u: User) => {
|
||||
try {
|
||||
const token = await startImpersonation(arcadia, u.id)
|
||||
// Swap the session to the impersonation token, then land on the user's
|
||||
// own home. The "Viewing as…" banner appears from here on.
|
||||
beginImpersonation(token.access_token)
|
||||
toast.info(`Now viewing as ${u.email}`)
|
||||
navigate("/")
|
||||
} catch (err) {
|
||||
toast.error(errorMessage(err, `impersonate ${u.email}`))
|
||||
}
|
||||
}
|
||||
|
||||
const [search, setSearch] = useState("")
|
||||
const [statusFilter, setStatusFilter] = useState<"all" | UserStatus>("all")
|
||||
const [editor, setEditor] = useState<{ mode: "create" } | { mode: "edit"; user: User } | null>(null)
|
||||
@@ -335,6 +352,7 @@ function UsersPanel({
|
||||
setPendingDelete,
|
||||
setDetailUser,
|
||||
toast,
|
||||
impersonate,
|
||||
})}
|
||||
triggerDataAction={`user-${u.id}-actions`}
|
||||
/>
|
||||
@@ -502,9 +520,11 @@ function userRowActions(
|
||||
setPendingDelete: (u: User | null) => void
|
||||
setDetailUser: (u: User | null) => void
|
||||
toast: ReturnType<typeof useToast>
|
||||
impersonate: (u: User) => void
|
||||
},
|
||||
): ActionItem[] {
|
||||
const { arcadia, refresh, setEditor, setPendingDelete, setDetailUser, toast } = ctx
|
||||
const { arcadia, refresh, setEditor, setPendingDelete, setDetailUser, toast, impersonate } =
|
||||
ctx
|
||||
const items: ActionItem[] = []
|
||||
|
||||
items.push({
|
||||
@@ -515,6 +535,18 @@ function userRowActions(
|
||||
onSelect: () => setDetailUser(u),
|
||||
})
|
||||
|
||||
// Support-facing: act as this user. Only meaningful for a user who can sign
|
||||
// in; the server re-checks eligibility and 403s if not allowed.
|
||||
if (u.status === "active") {
|
||||
items.push({
|
||||
id: "impersonate",
|
||||
label: "Impersonate",
|
||||
icon: <Eye className="size-4" />,
|
||||
dataAction: `user-${u.id}-impersonate`,
|
||||
onSelect: () => impersonate(u),
|
||||
})
|
||||
}
|
||||
|
||||
items.push({
|
||||
id: "edit",
|
||||
label: "Edit",
|
||||
|
||||
Reference in New Issue
Block a user